什么是Event Loop
Node.js 可以处理很多请求任务(如网络/文件等),将这些任务(也叫回调函数)分成至少 6 类,按先后顺序调用,因此将事件分为六个阶段。Node.js 会不停的从 1 ~ 6 循环处理各种事件,这个过程叫做事件循环(Event Loop)。
Event Loop的六个阶段
┌───────────────────────┐
┌─>│ 1. timers │ 主要处理 setTimeout 任务
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ 2. I/O callbacks │ 该阶段不用管
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ 3. idle, prepare │ 该阶段不用管
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ 停留时间最长 │ incoming: │
│ │ 4. poll │<───────────────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ 5. check │ 主要处理 setImmediate 任务
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ 6. close callbacks │ 该阶段不用管
└───────────────────────┘
上图一共有六个阶段,每个阶段都有一个「先入先出队列」,队列里存储要执行的回调函数,当 Event Loop 达到某个阶段时,会在这个阶段进行一些「特殊的操作」,然后执行这个阶段的队列里的所有回调函数,以下情况会结束这个阶段,进入下一个阶段:
- 队列的操作全被执行完了
- 执行的回调函数的个数到达设定的最大值,Event Loop必须进入下一个阶段。
我们只关心以下三个阶段:
- timer计时器。
setTimeout回调在此阶段执行 - poll轮询。检查系统事件。大部分时间,Node.js都停在poll轮询阶段。大部分事件都在poll阶段被处理,如文件,网络请求。一般在poll阶段有规定最大等待时间,比如停留3s后,还是会进入check阶段,再重新循环。
- check检查。
setImmediate回调。
nextTick (只有Node.js里有,浏览器无)
问:process.nextTick(fn) 的 fn 会在什么时候执行呢?
答:
- 在 Node.js 11 之前,会在每个阶段的末尾集中执行(俗称队尾执行)。
- 在 Node.js 11 之后,会在每个阶段的任务间隙执行(俗称插队执行)。
浏览器跟 Node.js 11 之后的情况类似。可以用 window.queueMicrotask 模拟 nextTick,进行插队执行。
Promise
问:Promise.resolve(1).then(fn) 的 fn 会在什么时候执行?
答:转换成 process.nextTick(fn) 去判断。
async / await
改写成 Promise,再转成nextTick写法
async1()
await async2()
fn()
在 await 之前的代码,会同步执行,在 await 后面的代码,可以改写为async2().then(fn()),再改写为:先执行 async2(),再nextTick(fn())
面试题1
在 Node.js 里运行以下代码会输出什么?
setTimeout(() => {
console.log('s1')
})
setImmediate(() => {
console.log('s2')
})
答案:先输出s1还是s2都有可能,这要看是先启动JS引擎,还是先启动libuv。
- 如果先启动了JS引擎,那么JS执行到setTimeout会塞入timers阶段,将setImmediate塞入check阶段,然后再启动libuv开始事件循环,这时候先经历timers阶段,输出s1,再经历check阶段,输出s2.
- 如果先启动了libuv,那么大部分时间都停留在poll阶段,JS启动后将setTimeout会塞入timers阶段,将setImmediate塞入check阶段,poll->check->timers,先输出s2,再输出s1
面试题2
async function async1(){
console.log('1')
async2().then(()=>{ // f2
console.log('2')
})
}
async function async2(){
console.log('3') // 3
}
console.log('4')
setTimeout(function(){ // f1
console.log('5')
},0)
async1();
new Promise(function(resolve){ // f3
console.log('6')
resolve();
}).then(function(){ // f4
console.log('7')
})
console.log('8') // 5
//4 1 3 6 8 2 7 5
输出4 1 3 6 8 2 7 5。执行过程如下:
同步:
- 先打印输出4
- 把 函数 f1 放到 宏任务队列
- 执行
async1:打印输出1,执行async2,打印输出3,将 f2 放入 微任务队列里 - 立刻执行 f3 ,打印输出6,
resolve()使得 f4 放入 微任务队列里 - 打印输出8。此时宏任务队列 [ f1 ],微任务队列 [ f2,f4 ] 异步:
- 先执行微任务队列,执行 f2,打印输出2
- 执行 f4 ,打印输出7
- 执行宏任务队列里的 f1 ,打印输出5