答题大纲
下面就跟着这个大纲走,每个点来说一下吧~
JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
macro-task大概包括:
micro-task大概包括:
整体执行,我画了一个流程图:
GitHub
总的结论就是,执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。举个例子:
结合流程图理解,答案输出为:async2 end => Promise => async1 end => promise1 => promise2 => setTimeout 但是,对于async/await ,我们有个细节还要处理一下。如下:
我们知道async
隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。我们来看个例子:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// 旧版输出如下,但是请继续看完本文下面的注意那里,新版有改动
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
分析这段代码:
script start
。async2 end
,此时将会保留async1函数的上下文,然后跳出async1函数。Promise
。遇到then,产生第一个微任务script end
promise1
,该微任务遇到then,产生一个新的微任务promise2
,当前微任务队列执行完毕。执行权回到async1let promise_ = new Promise((resolve,reject){ resolve(undefined)})
执行完成,执行await后面的语句,输出async1 end
setTimeout
新版的chrome浏览器中不是如上打印的,因为chrome优化了,await变得更快了,输出为:
// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
但是这种做法其实是违法了规范的,当然规范也是可以更改的,这是 V8 团队的一个 PR ,目前新版打印已经修改。知乎上也有相关讨论,可以看看 https://www.zhihu.com/question/268007969
我们可以分2种情况来理解:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
})
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
输出为:
// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout
此时执行完awit并不先把await后面的代码注册到微任务队列中去,而是执行完await之后,直接跳出async1函数,执行其他代码。然后遇到promise的时候,把promise.then注册为微任务。其他代码执行完毕后,需要回到async1函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中,注意此时微任务队列中是有之前注册的微任务的。所以这种情况会先执行async1函数之外的微任务(promise1,promise2),然后才执行async1内注册的微任务(async1 end). 可以理解为,这种情况下,await 后面的代码会在本轮循环的最后被执行. 浏览器中有事件循环,node 中也有,事件循环是 node 处理非阻塞 I/O 操作的机制,node中事件循环的实现是依靠的libuv引擎。由于 node 11 之后,事件循环的一些原理发生了变化,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。
浏览器中有事件循环,node 中也有,事件循环是 node 处理非阻塞 I/O 操作的机制,node中事件循环的实现是依靠的libuv引擎。由于 node 11 之后,事件循环的一些原理发生了变化,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。
node 中也有宏任务和微任务,与浏览器中的事件循环类似,其中,
macro-task 大概包括:
micro-task 大概包括:
先看一张官网的 node 事件循环简化图:
图中的每个框被称为事件循环机制的一个阶段,每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。
因此,从上面这个简化图中,我们可以分析出 node 的事件循环的阶段顺序为:
输入数据阶段(incoming data)->轮询阶段(poll)->检查阶段(check)->关闭事件回调阶段(close callback)->定时器检测阶段(timers)->I/O事件回调阶段(I/O callbacks)->闲置阶段(idle, prepare)->轮询阶段...
日常开发中的绝大部分异步任务都是在 poll、check、timers 这3个阶段处理的,所以我们来重点看看。
timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。
poll 是一个至关重要的阶段,poll 阶段的执行逻辑流程图如下:
如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到 timers 阶段。
如果没有定时器, 会去看回调函数队列。
check 阶段。这是一个比较简单的阶段,直接执行 setImmdiate 的回调。
process.nextTick 是一个独立于 eventLoop 的任务队列。
在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。
看一个例子:
setImmediate(() => {
console.log('timeout1')
Promise.resolve().then(() => console.log('promise resolve'))
process.nextTick(() => console.log('next tick1'))
});
setImmediate(() => {
console.log('timeout2')
process.nextTick(() => console.log('next tick2'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
timeout1=>timeout2=>timeout3=>timeout4=>next tick1=>next tick2=>promise resolve
timeout1=>next tick1=>promise resolve=>timeout2=>next tick2=>timeout3=>timeout4
这里主要说明的是 node11 前后的差异,因为 node11 之后一些特性已经向浏览器看齐了,总的变化一句话来说就是,如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行对应的微任务队列,一起来看看吧~
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
timer1=>promise1=>timer2=>promise2
timer1=>promise1=>timer2=>promise2
timer1=>timer2=>promise1=>promise2
setImmediate(() => console.log('immediate1'));
setImmediate(() => {
console.log('immediate2')
Promise.resolve().then(() => console.log('promise resolve'))
});
setImmediate(() => console.log('immediate3'));
setImmediate(() => console.log('immediate4'));
immediate1=>immediate2=>promise resolve=>immediate3=>immediate4
immediate1=>immediate2=>immediate3=>immediate4=>promise resolve
setImmediate(() => console.log('timeout1'));
setImmediate(() => {
console.log('timeout2')
process.nextTick(() => console.log('next tick'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
timeout1=>timeout2=>next tick=>timeout3=>timeout4
timeout1=>timeout2=>timeout3=>timeout4=>next tick
以上几个例子,你应该就能清晰感受到它的变化了,反正记着一个结论,如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行对应的微任务队列。
两者最主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,而nodejs中的微任务是在不同阶段之间执行的。
node的事件循环中,首次进入事件循环时,在poll阶段,有可能会跳到check阶段执行回调,但是check阶段在poll阶段之后,那么poll阶段是如何知道check阶段有没有回调需要执行的呢?