Microtasks(微任务)是事件循环中一类优先级比较高的任务,本文通过一个有趣的例子探索其运行时机。从两年前被动接受知识 "当浏览器JS引擎调用栈弹空的时候,才会执行 Microtasks 队列",到两年后主动深入探索源码后了解到的 "当 V8 执行完调用要返回 Blink 时,由于 MicrotasksScope 作用域失效,在其析构函数中检查 JS 调用栈是否为空,如果为空就会运行 Microtasks。"。同时文章中介绍了用于探索浏览器运行原理的一些工具。
一个有趣的例子
刚学前端那会学习事件循环,说事件循环存在的意义是由于 JavaScript 是单线程的,所以需要事件循环来防止JS阻塞,让网络请求等I/O操作不阻塞主线程。
而 Microtasks 是一类优先级比较高的任务,我们不能像 Macrotasks(宏任务) 一样插入 Macrotasks 队列末端,等待多个事件循环后才执行,而需要插入到 Microtasks 的队列里面,在本轮事件循环中执行。
比如下面这个有趣的例子:
document.body.innerHTML = `
<button id="btn" type="button">btn</button>
`;
const button = document.getElementById('btn')
button.addEventListener('click',()=>{
Promise.resolve().then(()=>console.log('promise resolved 1'))
console.log('listener 1')
})
button.addEventListener('click',()=>{
Promise.resolve().then(()=>console.log('promise resolved 2'))
console.log('listener 2')
})
// 1. 手动点击按钮
// button.click() // 2. 解开这句注释,用JS触发点击行为
当我手动点击按钮的时候,大家觉得浏览器的输出是下面的A还是B?
- A. listener1 -> promise resolved 1 -> listener2 -> promise resolved 2
- B. listener1 -> listener2 -> promise resolved 1 -> promise resolved 2
大家可以在这里试一下:
https://codesandbox.io/static/img/play-codesandbox.svg
当我将上面代码中的最后一行注释打开,使用JS触发点击行为的时候,浏览器的输出是A还是B?
大家觉得上面1、2两种情况的输出顺序是否一样?
答案非常有意思
- 当我们使用1. 手动点击按钮时,浏览器的输出是A
- 当我们使用2. 用JS触发点击行为时,浏览器的输出是B
被动接受知识
为什么会出现这种情况呢? 这个 Microtasks 的运行时机有关。两年前当我带着这个问题搜索资料并询问大佬的时,大佬告诉我:
当浏览器JS引擎调用栈弹空的时候,才会执行Microtasks队列
按照这个结论,我使用 Chrome Devtool 中的 Performance 做了一次探索
人工点击按钮
人工点击的时候输出为 listener1 -> promise resolved 1 -> listener2 -> promise resolved 2 。
- 从上图中我们可以看到,一次点击事件之后,浏览器会调用 Function Call 进入JS引擎,执行 listener1,输出
listener1
。 - 弹栈时发现JS调用栈为空,这时候就会执行 Microtasks 队列中的所有 Microtask,输出
promise resolved 1
。 - 接着浏览器调用 Function Call 进入JS引擎,执行 listener2,输出
listener 2
。 - 弹栈时发现JS调用栈为空,这时候就会执行 Microtasks 队列中的所有Microtask,输出
promise resolved 2
。
JS触发点击事件
在JS代码中触发点击时输出为 listener1 -> listener2 -> promise resolved 1 -> promise resolved