本篇博客讲的东西偏底层,较难理解。虽然有的地方不够精准和全面,但是我觉得对于理解js中的异步来说已经够了,所以没有再深究一些概念(比如浏览器在这个过程中充当的角色)。
所谓的单线程,可以简单理解为做事情讲究先来后到,要做后面的事情,你得等前面的事情做完—–不管它需要多久。 既然如此,js引擎为何还要采取这种单线程的机制呢? js主要是与用户互动,这个过程涉及到对DOM节点的操作,如果js是多线程的,一个在节点上添加内容,一个要对这个dom节点进行删除,到底是以哪个为准?所以这就是为什么js从一出现就秉承着单线程的运行机制。 另外还要注意:
“为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质”
很显然,单线程会带来一个问题:就是代码执行的阻塞。比如:排在前面的任务如果耗时长,则后面的任务不得不一直等待它。 如果说耗时长是因为计算量大、cpu一直忙着计算的话倒也还好,可事实是——大部分时间浪费在了IO上(ajax从网络上获取数据),还有其他的如鼠标点击、setTimeout等等。因此这里提出了同步任务和异步任务的概念。
在js中,可以将同步和异步简单理解为执行顺序的问题。
即上面所说的后面等待前面。同步对应了同步任务(synchronous),即可以按照正常顺序执行的任务,比如加载页面骨架等。
即把耗时长的任务挂起,先执行耗时短的,再回过头执行耗时长的。 异步对应了异步任务(asynchronous),即不适合按照正常顺序执行的任务,主要包括:
事件循环机制离不开执行栈和任务队列的相互配合。js中将同步任务放到主线程上执行,形成“执行栈”;异步任务则放到任务队列中。
任务队列的分类标准之一:
一个线程可以拥有多个任务队列。每一个任务队列都对应某一任务源,并包含了一堆来自该任务源的任务。任务源是什么?像setTimeout/Promise/DOM事件/AJAX等都是任务源,来自同类任务源的任务我们称它们是同源的,比如setTimeout与setInterval就是同源的。
任务队列的分类标准之二:
在ES6中,我们用另一种方式对任务队列进行分类。 宏任务: 即macro-task,包括整体代码script,setTimeout,setInterval、AJAX、用户I\O 等。宏任务会对应地进入宏任务队列中; 微任务: 即micro-task,包括Promise,process.nextTick(callback)(可以理解为node.js版的setTimeOut)。微任务会对应地进入微任务队列中。
总的来说,事件循环的顺序,决定了js代码执行的顺序。
<script>
包裹的整体代码(这是第一个宏任务),标志着第一次循环开始。在整体代码的执行过程中,同步任务照旧执行,异步任务分发到对应的任务队列中;PS:读取任务时,会执行这些任务指定的回调函数,并且要注意:若回调函数中又有宏任务,则该宏任务会被安排到下一轮循环中。
下面通过三个由易到难的例子来理解上面所说的过程。
例1
setTimeout(() => {
task()
},3000)
sleep(10000000)
分析: 跑一下代码,会发现控制台执行task()需要的时间远远超过3秒,这就说明我们有的人理解的”setTimeout的第二个参数指定了多长时间后执行回调函数”的说法是错误的。 让我们来分析一下这个过程:
<script>
中的整段代码作为第一个宏任务,进入主线程。即开启第一次事件循环;现在,我们知道setTimeout的回调函数是一开始就注册进event table的,但是那时并未进入任务队列—-要经过一定的时间,而这个时间由第二个参数来指定。也就是说,第二个参数指定的是“多长时间后将回调函数放入到任务队列中”。
另外,即使回调函数已经进入队列,也得先等主线程的执行栈清空后才有可能轮到自己。
我们还经常遇到setTimeout(fn,0)
(或者干脆没有指定第二个参数)这样的代码,这是不是意味着可以立即执行呢?
不是。setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是注册进event table的同时就将任务放入队列中,只要主线程执行栈内的同步任务全部执行完成,且此时没有微任务队列,那么该任务就会马上压栈并执行。
例2
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
分析:
<script>
中的整段代码作为第一个宏任务,进入主线程。即开启第一次事件循环;例3
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
分析:
第一轮事件循环:
a) 整段`<script>`代码作为第一个宏任务进入主线程,即开启第一轮事件循环
b) 遇到console.log,立即执行。输出:1
c) 遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务队列中。
我们将其标记为setTimeout1
d) 遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务
队列中。记为process1
e) 遇到new Promise、Promise,立即执行;then回调函数放入Event table中注册,然后
被分发到微任务队列中。记为then1。
输出: 7
f) 遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务队列中。
我们将其标记为setTimeout2
此时第一轮事件循环宏任务结束,下表是第一轮事件循环宏任务结束时各任务队列的情况
可以看到第一轮事件循环宏任务结束后微任务事件队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行
g)、执行process1。输出:6
h)、执行then1。输出:8
第一轮事件循环正式结束!
第二轮事件循环:
a)、第二轮事件循环从宏任务setTimeout1开始。遇到console.log,立即执行。输出: 2
b)、遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务
队列中。记为process2
c)、遇到new Promise,立即执行;then回调函数放入Event table中注册,然后被分发到
微任务队列中。记为then2。输出: 5
此时第二轮事件循环宏任务结束,下表是第二轮事件循环宏任务结束时各任务队列的情况
可以看到第二轮事件循环宏任务结束后微任务事件队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行
d)、执行process2。输出:3
e)、执行then2。输出:5
第二轮事件循环正式结束!
第三轮事件循环:
a)、第三轮事件循环从宏任务setTimeout2开始。遇到console.log,立即执行。输出: 9
d)、遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务
队列中。记为process3
c)、遇到new Promise,立即执行;then回调函数放入Event table中注册,然后被分发到
微任务队列中。记为then3。输出: 11
此时第三轮事件循环宏任务结束,下表是第三轮事件循环宏任务结束时各任务队列的情况
可以看到第二轮事件循环宏任务结束后微任务队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行
d)、执行process3。输出:10
e)、执行then3。输出:12
第二轮事件循环正式结束! 执行栈清空,任务队列清空,事件循环正式结束!
参考: https://segmentfault.com/a/1190000017970432 http://www.ruanyifeng.com/blog/2014/10/event-loop.html