Martin

爱设计,爱创造|To design and create

深入理解宏任务与微任务

本文章以浏览器环境为主。

先从三道题目说起

1. 宏任务有哪些?微任务有哪些?请各举几个。
2. 定时器真的是按照设定的时间定时执行吗?为什么?
3. 什么是宏任务?什么是微任务?它们之间怎么界定?请说说你的理解。

这三道题由浅入深,读者可以先思考一下。

好,下面我们围绕这三个问题进行展开。

记忆题?

相信很多了理解过宏任务与微任务的读者,都可以很快地回答第一题。

宏任务
  • setTimeout
  • setInterval
  • setImmediate(Node)
  • requestAnimationFrame
微任务
  • MutationObserver
  • Promise.then(catch、finally)
  • process.nextTick(Node)

这道我们姑且可以死记硬背。如果面试被问的话,肯定是道送分,但这仅仅是个开始。

相信面试官或者屏幕前的你不止满足于此❤️。我们继续看下一题。

经常迟到者

定时器真的是按照设定的时间定时执行吗?为什么?

答案肯定是不一定定时执行。

为什么呢?我们先看一个例子:

1
2
3
4
5
6
7
8
var start = Date.now()
console.log('启动定时器前...')
setTimeout(function () {
console.log('定时器执行了', (Date.now() - start)+'ms')
}, 200)
console.log('启动定时器后...')
// 做一个长时间的工作
for (var i = 0; i < 1000000000; i++) {}

然后这是笔者在浏览器上执行的结果:

1
2
3
4
> 启动定时器前...
> 启动定时器后...
> undefined
> 定时器执行了 2415ms

不禁感叹:这个差距实在……为什么它会放我鸽子?

没有没有,不敢放你鸽子。它只是在路上耽误了点时间。

JS 引擎在调用 setTimeout 之后,会跟它的上头(宿主)注册一个任务。上头收到这个任务之后会先计时,时间到了(200ms)后发现唯一能执行这个任务的劳工(JS 引擎)还在做别的事情,只好记在 TODO List 上(押入任务队列队末)。最后,劳工按顺序一步步把上头分配 TODO LIST 做完(包括这个任务)。

我们重新整理一下刚刚所说的:

  1. JS 引擎调用 setTimeout,然后继续执行下面的代码
  2. 宿主计时(会分配给其他线程处理)
  3. 时间到了,宿主把回调押入任务队列队尾
  4. 宿主按照队列顺序叫 JS 引擎一个个处理任务
  5. 回调被执行,任务队列清空

而这里所说的任务(task)就是宏任务。

JS 引擎多忙啊,可惜同一时间只能做一件事(单线程)。所以,就算真的到时间了,也要等 JS 引擎空闲的时候(执行完已有的代码)才能处理。中间的时长,我们很难估定。

我们再看一个比较极端的例子:

1
2
3
setTimeout(() => console.log('hello, 初次见面~'), 0);
// 阻塞线程
while(true) {}

这个例子很容易理解:估计这辈子都见不了面了:(

做事有条有理的劳工

至于第三题,上面已经比较清楚的介绍了宏任务,那么笔者现在就来说一下微任务。

其实,JS 引擎很负责很认真,最重要的是很有条理:每次执行宿主给的任务,它都会新开一个自己的 TODO List(微任务队列 microtask queue),专门记录一些不能在这个任务中按代码顺序立即能完成的事情。而这个 TODO List 通常会在这个任务(如当前执行脚本)要结束时才会去着手处理。

以上描述的“不能立即完成的事情”(请理解为一些异步任务),就是微任务。比如 Promise.then

1
2
Promise.resolve(2).then((v) => console.log(v));
console.log(1);

会先输出1,然后输出2。

两者的区别

其实宏任务跟微任务的存在性相似,大部分都是为了异步。至于为什么要有微任务,其实跟为什么要有线程很像。微任务存在目的主要是为了降低操作粒度,减少切换上下文的开销,保证更快被执行。

下面这段话节选自《Tasks, microtasks, queues and schedules》:

Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.

请读者分析分析一下两个例子:

1
2
3
4
5
6
7
// Macrotasks,不会造成死循环
function callback() {
console.log('callback');
setTimeout(callback,0);
}

callback();

上面不停地产生新的宏任务,旧的被执行完之后去执行新的。

1
2
3
4
5
6
7
// Microtasks,同样会造成死循环,页面无响应
function callback() {
console.log('callback');
Promise.resolve().then(callback);
}

callback();

这个例子也差不多,但却用了微任务,导致微任务队列一直有任务等待执行,JS 引擎阻塞。

微任务先行

当你看到这里的时候,就差不多理解了为什么存在微任务,为什么微任务先行。然后我们可以以一张图总结一下宏任务与微任务的关系。

image

通过这幅图,相信读者对它们的执行顺序有最直观的理解:微任务队列被清空后,再到宏任务队列被清空。

Ref

  1. Tasks, microtasks, queues and schedules
  2. JavaScript线程机制与事件机制
  3. 我与Microtasks的前世今生之一眼望穿千年

Proudly powered by Hexo and Theme by Hacker
© 2021 Martin Yong