各位观众老爷,大家好!欢迎来到今天的JS任务队列脱口秀。今天咱们聊聊setTimeout(..., 0)
和queueMicrotask()
,这两个家伙,看起来都是“立即执行”,但实际上肚子里弯弯绕绕可多了。准备好,咱们开始上车!
第一幕:setTimeout(…, 0)——延迟退休的老干部
首先,我们来认识一下setTimeout(..., 0)
。这家伙,表面上说“0毫秒后执行”,但实际上,它可不是立即执行。它会把你的任务扔到宏任务队列(Macrotask Queue)里,等着浏览器“处理完手头的事儿”再说。
宏任务队列里都有些什么妖魔鬼怪呢?
宏任务类型 | 说明 |
---|---|
setTimeout |
设定一个定时器,到期后执行回调函数。 |
setInterval |
循环执行回调函数,直到被clearInterval 清除。 |
setImmediate |
(Node.js 特有) 立即执行回调函数,但会在事件循环的下一个迭代中执行。 |
I/O 操作 | 比如读取文件、发送网络请求等。 |
UI 渲染 | 浏览器需要渲染页面的时候,也会把渲染任务放到宏任务队列里。 |
用户交互 | 比如用户点击、滚动等事件。 |
script(初始JS代码) | 脚本的执行本身就是一个宏任务。 |
console.log("任务开始");
setTimeout(function() {
console.log("setTimeout 0:老干部上班了");
}, 0);
console.log("任务结束");
// 输出顺序:
// 任务开始
// 任务结束
// setTimeout 0:老干部上班了
看,setTimeout(..., 0)
里的任务,虽然设置了0延迟,但还是排在了console.log("任务结束")
后面执行。这是因为,JavaScript引擎会先执行完当前的同步代码,然后才会去宏任务队列里找活干。
第二幕:queueMicrotask()——特事特办的VIP
接下来,隆重介绍我们的VIP贵宾:queueMicrotask()
。这家伙可不简单,它会把你的任务扔到微任务队列(Microtask Queue)里。
微任务队列里的都是些什么大佬呢?
微任务类型 | 说明 |
---|---|
Promise.then |
当Promise的状态变为fulfilled或rejected时,会执行对应的then 或catch 回调函数。 |
async/await |
async 函数会返回一个Promise,await 会暂停async 函数的执行,直到Promise resolved/rejected。await 之后的代码可以看作是Promise.then 的回调函数。 |
MutationObserver |
监听DOM树的变化,并在DOM变化后执行回调函数。 |
queueMicrotask |
将一个函数排入微任务队列,以便在当前任务结束之后、渲染之前立即执行。 |
process.nextTick |
(Node.js 特有) 将一个函数排入下一个事件循环迭代的开始阶段执行。 严格来说,它不是标准的微任务,但行为类似。 |
console.log("任务开始");
queueMicrotask(function() {
console.log("queueMicrotask:VIP贵宾驾到");
});
console.log("任务结束");
// 输出顺序:
// 任务开始
// 任务结束
// queueMicrotask:VIP贵宾驾到
和setTimeout(..., 0)
一样,queueMicrotask
里的任务也不是立即执行。但是,它的优先级比宏任务高!也就是说,JavaScript引擎会先执行完当前的同步代码,然后立即执行微任务队列里的所有任务,才会去宏任务队列里找活干。
第三幕:宏任务 vs 微任务——一场速度与激情的较量
现在,我们把setTimeout(..., 0)
和queueMicrotask()
放在一起,看看会发生什么:
console.log("任务开始");
setTimeout(function() {
console.log("setTimeout 0:老干部慢悠悠");
}, 0);
queueMicrotask(function() {
console.log("queueMicrotask:VIP风风火火");
});
console.log("任务结束");
// 输出顺序:
// 任务开始
// 任务结束
// queueMicrotask:VIP风风火火
// setTimeout 0:老干部慢悠悠
看到了吧?即使setTimeout(..., 0)
设置了0延迟,queueMicrotask()
里的任务还是会先执行。这就是微任务的优先级高于宏任务的体现。
第四幕:Event Loop——幕后大Boss
这一切的幕后推手,就是Event Loop(事件循环)。Event Loop就像一个调度员,负责协调宏任务队列和微任务队列的工作。它的工作流程大致如下:
- 执行栈执行全局Script代码,这是第一个宏任务。
- 宏任务执行过程中,可以产生新的宏任务和微任务。
- 当宏任务执行完毕,检查微任务队列,依次执行队列中的所有微任务。
- 当微任务队列为空时,浏览器可能会更新渲染。
- 选取下一个宏任务执行,回到步骤2,循环往复。
用一张图来表示:
+---------------------+
| Global Script | (宏任务)
+---------------------+
|
V
+---------------------+
| 执行栈清空 |
+---------------------+
|
V
+---------------------+
| 检查微任务队列 |
+---------------------+
|
V
+---------------------+
| 执行所有微任务 |
+---------------------+
|
V
+---------------------+
| 微任务队列为空? |
+---------------------+
| Yes No|
V |
+---------------------+ ^
| 更新渲染(可能) | |
+---------------------+ |
| |
V |
+---------------------+ |
| 选取下一个宏任务 |---+
+---------------------+
第五幕:Promise——微任务的扛把子
Promise
是微任务的典型代表。Promise.then
的回调函数会被添加到微任务队列中。
console.log("任务开始");
Promise.resolve().then(function() {
console.log("Promise.then:微任务大哥");
});
console.log("任务结束");
// 输出顺序:
// 任务开始
// 任务结束
// Promise.then:微任务大哥
再来一个更复杂的例子:
console.log("任务开始");
setTimeout(function() {
console.log("setTimeout 0:宏任务小弟");
}, 0);
Promise.resolve().then(function() {
console.log("Promise.then 1:微任务大哥1");
});
Promise.resolve().then(function() {
console.log("Promise.then 2:微任务大哥2");
});
queueMicrotask(function() {
console.log("queueMicrotask:微任务小弟");
});
console.log("任务结束");
// 输出顺序:
// 任务开始
// 任务结束
// Promise.then 1:微任务大哥1
// Promise.then 2:微任务大哥2
// queueMicrotask:微任务小弟
// setTimeout 0:宏任务小弟
注意,所有的微任务都会在同一个事件循环迭代中执行完毕,才会去执行下一个宏任务。而且,微任务的执行顺序是按照它们被添加到队列的顺序来的。
第六幕:async/await——Promise的语法糖
async/await
是Promise
的语法糖,它可以让异步代码看起来更像同步代码。await
后面的代码,实际上会被转换成Promise.then
的回调函数,也就是一个微任务。
async function myAsyncFunction() {
console.log("async函数开始");
await new Promise(resolve => setTimeout(resolve, 0)); // 注意这里是setTimeout,所以是宏任务
console.log("async函数结束");
}
console.log("任务开始");
myAsyncFunction();
console.log("任务结束");
// 输出顺序:
// 任务开始
// async函数开始
// 任务结束
// async函数结束
但是注意,这里await new Promise(resolve => setTimeout(resolve, 0))
, 虽然await
后面跟的是一个Promise
,但是这个Promise
内部使用了setTimeout
, 所以实际上这里是制造了一个宏任务。因此,“async函数结束”会在下一次事件循环中执行。
如果我们将setTimeout
改成queueMicrotask
或者直接resolve,那么结果就会不一样了:
async function myAsyncFunction() {
console.log("async函数开始");
await new Promise(resolve => queueMicrotask(resolve)); // queueMicrotask,微任务
console.log("async函数结束");
}
console.log("任务开始");
myAsyncFunction();
console.log("任务结束");
// 输出顺序:
// 任务开始
// async函数开始
// 任务结束
// async函数结束
或者:
async function myAsyncFunction() {
console.log("async函数开始");
await Promise.resolve(); // 直接resolve,微任务
console.log("async函数结束");
}
console.log("任务开始");
myAsyncFunction();
console.log("任务结束");
// 输出顺序:
// 任务开始
// async函数开始
// 任务结束
// async函数结束
在这种情况下,“async函数结束”会在当前事件循环中,在同步代码执行完毕后,立即执行。
第七幕:实际应用场景——选择困难症患者的福音
那么,setTimeout(..., 0)
和queueMicrotask()
在实际开发中有什么用呢?什么时候该用哪个呢?
-
setTimeout(..., 0)
:- 解耦任务: 如果你想把一个耗时的任务分解成多个小任务,避免阻塞UI线程,可以使用
setTimeout(..., 0)
。 - 延迟执行: 如果你想在浏览器完成渲染之后再执行某些操作,可以使用
setTimeout(..., 0)
。比如,你可能需要在DOM更新之后,再获取元素的尺寸。 - 模拟用户交互: 有些情况下,你可能需要模拟用户的交互行为。比如,你可能需要延迟一段时间后,再触发一个点击事件。
- 解耦任务: 如果你想把一个耗时的任务分解成多个小任务,避免阻塞UI线程,可以使用
-
queueMicrotask()
:- 确保任务在渲染前执行: 如果你需要在DOM更新之后,立即执行某些操作,而且这些操作不能被UI渲染打断,可以使用
queueMicrotask()
。比如,你可能需要在更新DOM之后,立即更新组件的状态。 - 避免状态不一致: 如果你有多个操作会修改同一个状态,为了避免状态不一致,可以使用
queueMicrotask()
把这些操作放在同一个微任务队列里。这样可以确保这些操作在一个事件循环迭代中全部执行完毕。 - 库的内部实现: 很多库会使用
queueMicrotask
来确保某些操作的原子性,比如在状态更新后立即触发回调函数。
- 确保任务在渲染前执行: 如果你需要在DOM更新之后,立即执行某些操作,而且这些操作不能被UI渲染打断,可以使用
总的来说,如果你对执行时机要求不高,或者希望把任务分解成多个小块,可以使用setTimeout(..., 0)
。如果你对执行时机要求很高,或者需要在渲染前执行某些操作,可以使用queueMicrotask()
。
第八幕:总结——记住这些要点
setTimeout(..., 0)
把任务扔到宏任务队列,queueMicrotask()
把任务扔到微任务队列。- 微任务的优先级高于宏任务。
- Event Loop负责协调宏任务队列和微任务队列的工作。
Promise.then
和async/await
都是微任务的典型代表。- 根据实际需求选择合适的API。
谢幕:彩蛋——一道思考题
最后,给大家留一道思考题:
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
queueMicrotask(() => console.log(4));
setTimeout(() => console.log(5));
Promise.resolve().then(() => console.log(6));
console.log(7);
请问,这段代码的输出顺序是什么? 欢迎在评论区留下你的答案!
今天的JS任务队列脱口秀就到这里了,感谢大家的观看! 我们下期再见!