带你彻底理解 Event Loop 的执行机制
JavaScript 是一个单线程语言,也就是说所有的任务必须一个一个排队执行,但是如果一个任务执行时间过长,便会阻塞后面的任务执行,因此需要将耗时时间长的任务作为异步任务来处理,以防止主线程阻塞,于是便有了 Event Loop 这个执行机制 。
看一下 js 的执行过程:
当执行到 setTimeout(fn , 1000) 或 setTimeout(fn , 0) 时,它会被放入 Event Table 中注册回调函数,再经过 1000ms 或 0毫秒时(实际最小延迟为4ms),回调函数被放入 Event Queue , 记住只有当主线程中的任务全部执行结束之后,才会去读取 Event Queue 中的回调结果,并放入到主线程中去执行 。
那什么是异步任务呢?
异步任务主要分为 宏任务 和 微任务:
- 宏任务: 代码块,setImmediate、setTimeout、setInterval、 I/O、UI交互事件 
- 微任务: Promise、progress.nextTick 、MutaionObserver 
另外 new Primise() 会立即调用里面的回调函数,应该属于同步任务
下面我们看看 Event Queue 中对于所有任务的执行循序是:
- 先执行代码块(主程序中的任务,个人理解是先执行所有的同步任务); 
- 然后开始执行队列中所有的微任务; 
- 然后再次执行下一个宏任务; 
- 然后重复第2步,直至任务队列为空 ; 
console.log('1');// setTimeout1setTimeout(function() {console.log('2');// promise1new Promise(function(resolve) {console.log('3');resolve();}).then(function() {console.log('4')})})// new Promise()new Promise(function(resolve) {console.log('5');resolve();}).then(function() {console.log('6')})// setTimeout2setTimeout(function() {console.log('7');// promise2new Promise(function(resolve) {console.log('8');resolve();}).then(function() {console.log('9')})})
根据上面的代码,分析执行步骤(chrome 环境中):
- 首先执行整体代码(主线程中的任务) 
- 然后开始执行队列中的微任 
- 然后再执行下一个宏任务 setTimeout1 
- 然后再执行 setTimeout1 代码块中的微任务 
- 然后再执行下一个宏任务 setTimeout2,此步骤跟 步骤3 同理: 输出 7,8,9 所以最终输出结果是: 1,5,6,2,3,4,7,8,9 
console.log(1)promise// 输出 1,5
promise.then// 输出 6
console.log('2');promise1// 输出 2,3
promise1.then// 输出 4
思考下面这段代码的输出顺序:
<div class="outer"><div class="inner"></div></div>var outer = document.querySelector('.outer');var inner = document.querySelector('.inner');new MutationObserver(function () {console.log('mutate');}).observe(outer, {attributes: true,});function onClick() {console.log('click');setTimeout(function () {console.log('timeout');}, 0);Promise.resolve().then(function () {console.log('promise');});outer.setAttribute('data-random', Math.random());}inner.addEventListener('click', onClick);outer.addEventListener('click', onClick);
当点击 inner div 时:
- 执行 onClick 回调执行: console.log('click'); 
- 然后执行微任务队列中的回调执行: - console.log('promise'); console.log('mutate'); 
- 由于 onClick 事件冒泡机制,再次执行 onClick (outer div 的点击事件),执行: console.log('click'); 
- 然后再次执行微任务队列中的回调:console.log('promise'); console.log('mutate'); 
- 然后再执行 setTimeout 所以输出结果为: 
onClick, promise, mutate, onClick, promise ,mutate, timeout, timeout 其他浏览器会有差异
具体执行过程,查看这篇文章中的例子 [Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) 有详细的执行步骤介绍
自己的理解
下面这张图是我自己的理解,具体过程图上都标准的很清楚,这里就不在赘述。
Node 环境
如果我们将上面的代码加入 progress.nextTick 放入到 node 环境中去执行,又会输出什么呢?
console.log('1');// setTimeout1setTimeout(function () {console.log('2');// promise1new Promise(function (resolve) {console.log('4');resolve();}).then(function () {console.log('5')})// process.nextTick1process.nextTick(function () {console.log('3');})})// promisenew Promise(function (resolve) {console.log('7');resolve();}).then(function () {console.log('8')})// process.nextTickprocess.nextTick(function () {console.log('6');})// setTimeout2setTimeout(function () {console.log('9');// promise2new Promise(function (resolve) {console.log('11');resolve();}).then(function () {console.log('12')})// process.nextTick2process.nextTick(function () {console.log('10');})})
原本以为按照上面的逻辑,可以得出结果输出:1,7,8,6,2,4,5,3,9,11,12,10
但是马上执行一遍又啪啪打脸啊,直接输出为:1,7,6,8,2,4,3,5,9,11,10,12 通过对比结果,发现 progress.nextTick 的优先级高于 promise
以上代码我是在 node 版本 v12.10.0 上测试的,但是在 v10.15.3 上测试,会发现输出:1,7,6,8,2,4,9,11,3,10,5,12 但是如果将第二个 setTimeout 设置延迟超过 1ms, 结果又会跟在版本 v12.10.0 上输出的一致!
我擦,这是个什么鬼,是不是有点懵逼,通过上面输出结果可以知道,当 setTimeout 延时相同,他们会合并(姑且这么理解),用代码来解释如下:
setTimeout(function () {console.log('2');// promise1new Promise(function (resolve) {console.log('4');resolve();}).then(function () {console.log('5')})// process.nextTick1process.nextTick(function () {console.log('3');})console.log('9');// promise2new Promise(function (resolve) {console.log('11');resolve();}).then(function () {console.log('12')})// process.nextTick2process.nextTick(function () {console.log('10');})})
合并之后,代码放在一起,那么执行的时候输出就是: 2,4,9,11,3,10,5,12 因此: 当 setTimeout 延时相同时,他们会合并在一起执行
下面这张图介绍了 Node 中 Event Loop 的执行机制:
资料
- [Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) 这篇文章不错,多看看 
- [Event Loop and the Big Picture](https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810) 
- [Timers, Immediates and Process.nextTick](https://blog.insiderattack.net/timers-immediates-and-process-nexttick-nodejs-event-loop-part-2-2c53fd511bb3) 
