抛转引玉
请看下面一个例子,思考一下打印输出顺序是什么?
console.log('Start');
setTimeout(function callback() {
console.log('setTimeout Callback');
}, 0);
Promise.resolve().then(function callback() {
console.log('Promise Callback');
});
console.log('End');
答案是(文章最后有解释)
Start
End
Promise Callback
setTimeout Callback
你能理解为什么是这个输出顺序吗?如果能,就说明你基本掌握了:
- 宏任务是什么,微任务是什么
- 宏任务和微任务的区别
- 宏任务和微任务的优先级
开始念经
宏任务和微任务以及他们的区别
宏任务和微任务的主要区别在于它们被 JavaScript 引擎处理的时间点。
宏任务包括:
setTimeout
setInterval
requestAnimationFrame
- UI 渲染(浏览器环境)
- 脚本所有同步代码的一次执行
微任务包括:
Promise.then
或Promise.catch
Vue3中的nextTick
MutationObserver
(浏览器环境)
宏任务通常来自于 JavaScript 引擎以外的部分,比如 DOM 事件、网络请求、定时器等。微任务通常来自于 JavaScript 代码的执行过程,最常见的是 Promise 的回调。
在 JavaScript 的事件循环中,每一次循环被称为一个宏任务。在每个宏任务中,JavaScript 引擎会首先执行宏任务队列中的一个任务,然后执行完所有的微任务队列。这意味着,如果一个宏任务在执行过程中产生了微任务,那么这些微任务会在下一个宏任务开始之前被执行。
宏任务与微任务的优先级
微任务的优先级高于宏任务。在同一次事件循环中,微任务总是在宏任务之后,但在下一个宏任务之前执行。就是说每当事件循环结束,会优先检查有没有微任务,如果有的话会优先执行微任务。
不懂就问:所有的回调都是微任务吗?
不是!在 JavaScript 中,setTimeout
和 setInterval
的回调函数产生的是宏任务,上文中requestAnimationFrame的回调也是宏任务(有兴趣可以自己查查,是干嘛的哈),而 Promise.then
或 Promise.catch
等异步操作的回调函数产生的是微任务,这是由 JavaScript 的事件循环和任务调度机制决定的。
不懂就问:为什么setTimeout
和 setInterval
的回调函数产生的是宏任务?
setTimeout
和 setInterval
是浏览器提供的 Web API,它们并不是 JavaScript 语言本身的一部分。当你调用 setTimeout
或 setInterval
并传入一个回调函数时,浏览器会启动一个定时器,当定时器到达指定的时间后,浏览器会将回调函数添加到宏任务队列。
宏任务队列是一个先进先出的队列,JavaScript 引擎会在每次事件循环时取出队列中的一个任务并执行。因此,setTimeout
或 setInterval
的回调函数会在下一次事件循环时被执行,这就是为什么它们产生的是宏任务。
不懂就问:微任务是宏任务执行过程中产生的是吗?
不完全是,微任务是可以在宏任务的执行过程中产生的,也可以在其它微任务的执行过程中产生
不懂就问:有可能发生微任务嵌套微任务,永远在执行微任务是吗?
在理论上,确实可以通过在微任务中连续创建新的微任务,从而创建出一连串的微任务。在这种情况下,JavaScript 引擎会在开始新的宏任务之前,先执行完所有已经排队的微任务。这意味着,如果你在一个微任务中不断地创建新的微任务,JavaScript 引擎可能会持续执行微任务,而没有机会开始新的宏任务,可能会导致 JavaScript 引擎耗尽资源,从而影响应用程序的性能。在实践中,我们应该尽量避免创建无限的任务循环,无论是宏任务还是微任务。
举个让浏览器出汗的例子:
//你将永远不会看到setTimeout Callback被打印出来
setTimeout(function callback() {
console.log('setTimeout Callback');
}, 0);
function recursiveMicrotask() {
Promise.resolve().then(() => {
console.log('Microtask executed');
recursiveMicrotask();
});
}
recursiveMicrotask();
解释开头的例子
相信经过上面的解释,你应该可以试试自己梳理一下开头的例子,问问自己:
有几个宏任务(有几次事件循环)?
有几个微任务?
输出顺序是啥?
执行过程是怎样的?
答案
以下是代码的执行过程:
-
首先,
console.log('Start')
会立即执行,因为它是同步代码。 -
然后,
setTimeout
函数会将其回调函数callback
添加到宏任务队列,等待执行。尽管setTimeout
的延迟时间设为 0,但是它的回调函数仍然需要等待当前的同步代码执行完毕,并且等待微任务队列清空后才能执行。 -
接下来,
Promise.resolve().then
会将其回调函数callback
添加到微任务队列。 -
然后,
console.log('End')
会立即执行,因为它是同步代码。 -
在当前宏任务的所有同步代码执行完毕后,JavaScript 引擎会开始执行微任务队列中的任务。因此,
Promise Callback
会被打印出来。 -
在微任务队列清空后,JavaScript 引擎会开始新的事件循环,执行宏任务队列中的任务。因此,
setTimeout Callback
会被打印出来。
所以,总的来说,这段代码会发生两次事件循环:一次是执行同步代码和微任务,另一次是执行 setTimeout
的回调函数。
有收获的话可以点个赞哟,激励博主更新更多干货~