面试题:说说事件循环机制(满分答案来了)

答题大纲

  1. 先说基本知识点,宏任务、微任务有哪些
  2. 说事件循环机制过程,边说边画图出来
  3. 说async/await执行顺序注意,能够把 chrome 的优化,作法实际上是违法了规范的,V8 团队的PR这些自信点说出来,显得你很好学,理解得很详细,很透彻。
  4. 把node的事件循环也说一下,重复一、二、3点,node中的第3点要说的是node11先后的事件循环变更点。

下面就跟着这个大纲走,每一个点来讲一下吧~html

浏览器中的事件循环

JavaScript代码的执行过程当中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另一些代码的执行。整个执行过程,咱们称为事件循环过程。一个线程中,事件循环是惟一的,可是任务队列能够拥有多个。任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。前端

macro-task大概包括:html5

  • script(总体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

micro-task大概包括:node

  • process.nextTick
  • Promise
  • Async/Await(实际就是promise)
  • MutationObserver(html5新特性)

总体执行,我画了一个流程图:git

GitHub

总的结论就是,执行宏任务,而后执行该宏任务产生的微任务,若微任务在执行过程当中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。举个例子: github

GitHub

结合流程图理解,答案输出为:async2 end => Promise => async1 end => promise1 => promise2 => setTimeout 可是,对于async/await ,咱们有个细节还要处理一下。以下:面试

async/await执行顺序

咱们知道async隐式返回 Promise 做为结果的函数,那么能够简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。可是咱们要注意这个微任务产生的时机,它是执行完await以后,直接跳出async函数,执行其余代码(此处就是协程的运做,A暂停执行,控制权交给B)。其余代码执行完毕后,再回到async函数去执行剩下的代码,而后把await后面的代码注册到微任务队列当中。咱们来看个例子:chrome

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
复制代码

分析这段代码:promise

  • 执行代码,输出script start
  • 执行async1(),会调用async2(),而后输出async2 end,此时将会保留async1函数的上下文,而后跳出async1函数。
  • 遇到setTimeout,产生一个宏任务
  • 执行Promise,输出Promise。遇到then,产生第一个微任务
  • 继续执行代码,输出script end
  • 代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务
  • 执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到async1
  • 执行await,实际上会产生一个promise返回,即
let promise_ = new Promise((resolve,reject){ resolve(undefined)})
复制代码

执行完成,执行await后面的语句,输出async1 end浏览器

  • 最后,执行下一个宏任务,即执行setTimeout,输出setTimeout

注意

新版的chrome浏览器中不是如上打印的,由于chrome优化了,await变得更快了,输出为:

// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
复制代码

可是这种作法实际上是违法了规范的,固然规范也是能够更改的,这是 V8 团队的一个 PR ,目前新版打印已经修改。 知乎上也有相关讨论,能够看看 www.zhihu.com/question/26…

咱们能够分2种状况来理解:

  1. 若是await 后面直接跟的为一个变量,好比:await 1;这种状况的话至关于直接把await后面的代码注册为一个微任务,能够简单理解为promise.then(await下面的代码)。而后跳出async1函数,执行其余代码,当遇到promise函数的时候,会注册promise.then()函数到微任务队列,注意此时微任务队列里面已经存在await后面的微任务。因此这种状况会先执行await后面的代码(async1 end),再执行async1函数后面注册的微任务代码(promise1,promise2)。

  2. 若是await后面跟的是一个异步函数的调用,好比上面的代码,将代码改为这样:

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 中也有,事件循环是 node 处理非阻塞 I/O 操做的机制,node中事件循环的实现是依靠的libuv引擎。因为 node 11 以后,事件循环的一些原理发生了变化,这里就以新的标准去讲,最后再列上变化点让你们了解来龙去脉。

宏任务和微任务

node 中也有宏任务和微任务,与浏览器中的事件循环相似,其中,

macro-task 大概包括:

  • setTimeout
  • setInterval
  • setImmediate
  • script(总体代码)
  • I/O 操做等。

micro-task 大概包括:

  • process.nextTick(与普通微任务有区别,在微任务队列执行以前执行)
  • new Promise().then(回调)等。

node事件循环总体理解

先看一张官网的 node 事件循环简化图:

GitHub

图中的每一个框被称为事件循环机制的一个阶段,每一个阶段都有一个 FIFO 队列来执行回调。虽然每一个阶段都是特殊的,但一般状况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操做,而后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。

所以,从上面这个简化图中,咱们能够分析出 node 的事件循环的阶段顺序为:

输入数据阶段(incoming data)->轮询阶段(poll)->检查阶段(check)->关闭事件回调阶段(close callback)->定时器检测阶段(timers)->I/O事件回调阶段(I/O callbacks)->闲置阶段(idle, prepare)->轮询阶段...

阶段概述

  • 定时器检测阶段(timers):本阶段执行 timer 的回调,即 setTimeout、setInterval 里面的回调函数。
  • I/O事件回调阶段(I/O callbacks):执行延迟到下一个循环迭代的 I/O 回调,即上一轮循环中未被执行的一些I/O回调。
  • 闲置阶段(idle, prepare):仅系统内部使用。
  • 轮询阶段(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎全部状况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的以外),其他状况 node 将在适当的时候在此阻塞。
  • 检查阶段(check):setImmediate() 回调函数在这里执行
  • 关闭事件回调阶段(close callback):一些关闭的回调函数,如:socket.on('close', ...)。

三大重点阶段

平常开发中的绝大部分异步任务都是在 poll、check、timers 这3个阶段处理的,因此咱们来重点看看。

timers

timers 阶段会执行 setTimeout 和 setInterval 回调,而且是由 poll 阶段控制的。 一样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。

poll

poll 是一个相当重要的阶段,poll 阶段的执行逻辑流程图以下:

GitHub

若是当前已经存在定时器,并且有定时器到时间了,拿出来执行,eventLoop 将回到 timers 阶段。

若是没有定时器, 会去看回调函数队列。

  • 若是 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

  • 若是 poll 队列为空时,会有两件事发生

    • 若是有 setImmediate 回调须要执行,poll 阶段会中止而且进入到 check 阶段执行回调
    • 若是没有 setImmediate 回调须要执行,会等待回调被加入到队列中并当即执行回调,这里一样会有个超时时间设置防止一直等待下去,一段时间后自动进入 check 阶段。
check

check 阶段。这是一个比较简单的阶段,直接执行 setImmdiate 的回调。

process.nextTick

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'));
复制代码
  • 在 node11 以前,由于每个 eventLoop 阶段完成后会去检查 nextTick 队列,若是里面有任务,会让这部分任务优先于微任务执行,所以上述代码是先进入 check 阶段,执行全部 setImmediate,完成以后执行 nextTick 队列,最后执行微任务队列,所以输出为timeout1=>timeout2=>timeout3=>timeout4=>next tick1=>next tick2=>promise resolve
  • 在 node11 以后,process.nextTick 是微任务的一种,所以上述代码是先进入 check 阶段,执行一个 setImmediate 宏任务,而后执行其微任务队列,再执行下一个宏任务及其微任务,所以输出为timeout1=>next tick1=>promise resolve=>timeout2=>next tick2=>timeout3=>timeout4

node 版本差别说明

这里主要说明的是 node11 先后的差别,由于 node11 以后一些特性已经向浏览器看齐了,总的变化一句话来讲就是,若是是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就马上执行对应的微任务队列,一块儿来看看吧~

timers 阶段的执行时机变化

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)
复制代码
  • 若是是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就马上执行微任务队列,这就跟浏览器端运行一致,最后的结果为timer1=>promise1=>timer2=>promise2
  • 若是是 node10 及其以前版本要看第一个定时器执行完,第二个定时器是否在完成队列中.
    • 若是是第二个定时器还未在完成队列中,最后的结果为timer1=>promise1=>timer2=>promise2
    • 若是是第二个定时器已经在完成队列中,则最后的结果为timer1=>timer2=>promise1=>promise2

check 阶段的执行时机变化

setImmediate(() => console.log('immediate1'));
setImmediate(() => {
    console.log('immediate2')
    Promise.resolve().then(() => console.log('promise resolve'))
});
setImmediate(() => console.log('immediate3'));
setImmediate(() => console.log('immediate4'));
复制代码
  • 若是是 node11 后的版本,会输出immediate1=>immediate2=>promise resolve=>immediate3=>immediate4
  • 若是是 node11 前的版本,会输出immediate1=>immediate2=>immediate3=>immediate4=>promise resolve

nextTick 队列的执行时机变化

setImmediate(() => console.log('timeout1'));
setImmediate(() => {
    console.log('timeout2')
    process.nextTick(() => console.log('next tick'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
复制代码
  • 若是是 node11 后的版本,会输出timeout1=>timeout2=>next tick=>timeout3=>timeout4
  • 若是是 node11 前的版本,会输出timeout1=>timeout2=>timeout3=>timeout4=>next tick

以上几个例子,你应该就能清晰感觉到它的变化了,反正记着一个结论,若是是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就马上执行对应的微任务队列。

node 和 浏览器 eventLoop的主要区别

二者最主要的区别在于浏览器中的微任务是在每一个相应的宏任务中执行的,而nodejs中的微任务是在不一样阶段之间执行的。

更多理解资料

参考资料

最后

  • 欢迎加我微信(winty230),拉你进技术群,长期交流学习...
  • 欢迎关注「前端Q」,认真学前端,作个有专业的技术人...

GitHub
相关文章
相关标签/搜索