event loop整理

宏任务和微任务

让我们从浏览器加载 script 说起,当浏览器加载完 script 之后,不考虑 script 标签的 defer 属性,script 将被立即执行。这时,我们就创建了一个宏任务。

在我们加载的代码中,可能有 click 事件的监听,也可能会发出网络请求。当这些操作触发我们埋下的回调函数后,相应的回调函数都会作为新的宏任务执行。

当然,在我们的宏任务的代码中,也可能会用到 Promise,MutationObserver 这些 api,他们的回调函数(或者 then 函数)都会在本轮宏任务结束之后,渲染进程工作之前执行,它们被称为微任务。

整个过程如下:

宏任务 => 微任务 => 渲染 => 宏任务 => 微任务 => 渲染 ...

这个循环往复的 过程在页面生命周期内一直存在,被称为 event loop。

一个例子

了解了什么是宏任务和微任务之后,我们来看一个经典的例子:

Promise.resolve().then(() => console.log('promise1 resolved')); // 1
Promise.resolve().then(() => console.log('promise2 resolved')); // 2
setTimeout(() => {
    console.log('set timeout3') // 7
    Promise.resolve().then(() => console.log('inner promise3 resolved')); // 8
}, 0);
setTimeout(() => console.log('set timeout1'), 0); // 9
setTimeout(() => console.log('set timeout2'), 0); // 10
Promise.resolve().then(() => console.log('promise4 resolved')); // 3
Promise.resolve().then(() => {
    console.log('promise5 resolved') // 4
    Promise.resolve().then(() => console.log('inner promise6 resolved'));  // 6
});
Promise.resolve().then(() => console.log('promise7 resolved')); // 5

解释上面的执行顺序:

  1. 每一个 setTimeout 都会放到下一轮宏任务中触发。
  2. 本轮微任务中产生的新的微任务,会被加到队尾,仍然在本轮微任务队列中执行完毕。解释了 inner promise6 resolved 在 promise7 resolved 后面
  3. 宏任务中产生的微任务,会在该轮宏任务结束之后,统一在微任务队列中执行,解释了 inner promise3 resolved 在 set timeout3 后面

于是得到下面的图,紫色代表宏任务,黄色代表微任务。

观察可以发现,在第一个宏任务中,除了创建 setTimeout 和 Promise 之外,是没执行什么同步代码的,现在我们在原先的代码最后再加一行:

...
Promise.resolve().then(() => console.log('promise7 resolved')); // 5
new Promise((reslove) => console.log('promise instance'));

此时,尽管代码被加在最后一行,promise instance 却会第一个打印出来,因为 new Promise 中传入的函数是立即执行的。也就是说在第一轮宏任务中执行的。

microtask 是會在每一輪 event loop 進行渲染之前會被觸發
且只要在 microtask queue 裡面還有東西的話,就會一直執行下去
直到整個 microtask queue 變成空的為止
也就是說在 microtask 執行的時候,又觸發 queue 新的 microtask 的話
這個新的 microtask 也是會在此輪 task 執行完之前執行,不會留到下一輪 task

Vue 的 nextTick

我们再看下宏任务,微任务,渲染的流程

https://user-gold-cdn.xitu.io/2019/8/21/16cb1d7bb4bd9fd2?imageslim

浏览器的 eventloop 是宏任务(dom 更新)=> 微任务 => 渲染,而 Vue 的 DOM 操作是在宏任务中进行的。这就解释了为什么 Vue 在完成本轮 DOM 更新之后,$nextTick 在本轮的微任务中执行回调函数,就可以确保拿到最新 DOM,尽管此时页面还没有将最新的 DOM 渲染到浏览器上。打开开发者工具,依次执行下面代码可以证明这一点:

// 模拟上一轮更新状态
document.body.style.background = 'yellow'; 

接下来模拟执行本轮更新操作:

document.body.style.background = 'red';
Promise.resolve().then(() =>{
    let start = Date.now();
    while(Date.now() - start < 3000) {
        console.log(document.body.style.background) // 由于 while 循环,这里拿到的 background 已经是最新的 red,但是页面显示还是黄色
    }
});
// 微任务执行结束之后,页面变成红色

上面的例子说明,在微任务中就可以拿到 Vue 本轮更新的DOM,而此时页面渲染的未必是最新的DOM。本文完。

参考资料:
https://yu-jack.github.io/2020/02/03/javascript-runtime-event-loop-browser/
https://juejin.im/post/6844903919789801486#heading-4
https://blog.insiderattack.net/javascript-event-loop-vs-node-js-event-loop-aea2b1b85f5c

posted @ 2021-02-27 11:50  饭特稠  阅读(186)  评论(0编辑  收藏  举报