本文章以浏览器环境为主。
先从三道题目说起
1. 宏任务有哪些?微任务有哪些?请各举几个。
2. 定时器真的是按照设定的时间定时执行吗?为什么?
3. 什么是宏任务?什么是微任务?它们之间怎么界定?请说说你的理解。
这三道题由浅入深,读者可以先思考一下。
好,下面我们围绕这三个问题进行展开。
记忆题?
相信很多了理解过宏任务与微任务的读者,都可以很快地回答第一题。
宏任务
- setTimeout
- setInterval
- setImmediate(Node)
- requestAnimationFrame
微任务
- MutationObserver
- Promise.then(catch、finally)
- process.nextTick(Node)
这道我们姑且可以死记硬背。如果面试被问的话,肯定是道送分,但这仅仅是个开始。
相信面试官或者屏幕前的你不止满足于此❤️。我们继续看下一题。
经常迟到者
定时器真的是按照设定的时间定时执行吗?为什么?
答案肯定是不一定定时执行。
为什么呢?我们先看一个例子:1
2
3
4
5
6
7
8var 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 做完(包括这个任务)。
我们重新整理一下刚刚所说的:
- JS 引擎调用
setTimeout
,然后继续执行下面的代码 - 宿主计时(会分配给其他线程处理)
- 时间到了,宿主把回调押入任务队列队尾
- 宿主按照队列顺序叫 JS 引擎一个个处理任务
- 回调被执行,任务队列清空
而这里所说的任务(task)就是宏任务。
JS 引擎多忙啊,可惜同一时间只能做一件事(单线程)。所以,就算真的到时间了,也要等 JS 引擎空闲的时候(执行完已有的代码)才能处理。中间的时长,我们很难估定。
我们再看一个比较极端的例子:1
2
3setTimeout(() => console.log('hello, 初次见面~'), 0);
// 阻塞线程
while(true) {}
这个例子很容易理解:估计这辈子都见不了面了:(
做事有条有理的劳工
至于第三题,上面已经比较清楚的介绍了宏任务,那么笔者现在就来说一下微任务。
其实,JS 引擎很负责很认真,最重要的是很有条理:每次执行宿主给的任务,它都会新开一个自己的 TODO List(微任务队列 microtask queue),专门记录一些不能在这个任务中按代码顺序立即能完成的事情。而这个 TODO List 通常会在这个任务(如当前执行脚本)要结束时才会去着手处理。
以上描述的“不能立即完成的事情”(请理解为一些异步任务),就是微任务。比如 Promise.then
:
1 | Promise.resolve(2).then((v) => console.log(v)); |
会先输出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 | // Microtasks,同样会造成死循环,页面无响应 |
这个例子也差不多,但却用了微任务,导致微任务队列一直有任务等待执行,JS 引擎阻塞。
微任务先行
当你看到这里的时候,就差不多理解了为什么存在微任务,为什么微任务先行。然后我们可以以一张图总结一下宏任务与微任务的关系。
通过这幅图,相信读者对它们的执行顺序有最直观的理解:微任务队列被清空后,再到宏任务队列被清空。