一、先看个生活场景
假设银行柜台只有一个窗口(JS主线程),来办业务的人分三种:
- 普通客户(同步任务):直接冲到窗口办业务(立即执行)
console.log('取号排队'); // 立即办理 - VIP客户(微任务) :拿金卡的客户,当前业务办完后必须立刻服务
Promise.resolve().then(() => console.log('VIP窗口办理')); // 插队办理 - 预约客户(宏任务) :取了号在等候区等待叫号
setTimeout(() => console.log('预约客户办理'), 0); // 最后办理
二、事件循环执行口诀
- 处理完所有普通客户(同步代码全执行)
- VIP客户立刻插队处理(清空微任务队列)
- 叫一个预约客户办理(执行一个宏任务)
重复这个流程直到关门(程序结束)。
常见宏任务和微任务:
| 宏任务(Macro-task) | 微任务(Micro-task) |
|---|---|
setTimeout/setInterval | Promise.then/catch/finally |
I/O 操作(如文件读取) | MutationObserver |
DOM 事件回调(如点击) | queueMicrotask |
requestAnimationFrame |
三、看几个实战案例
案例1:基础流程
console.log('普通客户1号');
setTimeout(() => console.log('预约客户1号'), 0);
Promise.resolve().then(() => console.log('VIP客户1号'));
console.log('普通客户2号');
输出结果:
普通客户1号
普通客户2号
VIP客户1号
预约客户1号
案例2:VIP中还有VIP
Promise.resolve().then(() => {
console.log('VIP客户1号');
Promise.resolve().then(() => console.log('VIP的家属'));
});
输出结果:
VIP客户1号
VIP的家属
(微任务队列必须清空到一滴都不剩)
案例3:宏任务嵌套
setTimeout(() => {
console.log('预约客户1号');
setTimeout(() => console.log('预约客户的家属'), 0);
}, 0);
输出结果:
预约客户1号
预约客户的家属
(每次只处理一个宏任务)
四、async/await 的猫腻
async函数其实就是披着马甲的Promise:
async function 买奶茶() {
console.log('1. 付钱');
await 等制作(); // 这里会生成微任务
console.log('3. 拿奶茶');
}
function 等制作() {
return new Promise(resolve => {
console.log('2. 开始制作');
resolve();
});
}
买奶茶();
输出结果:
1. 付钱
2. 开始制作
3. 拿奶茶
五、实际开发三大坑
坑1:以为点击事件能抢跑
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('统计点击量'));
console.log('按钮被点了');
});
点击后输出:
按钮被点了
统计点击量
(整个点击回调都是宏任务)
坑2:requestAnimationFrame 乱入
setTimeout(() => console.log('setTimeout'), 0);
requestAnimationFrame(() => console.log('RAF'));
可能输出:
RAF
setTimeout
(动画回调在渲染前执行,算特殊宏任务)
坑3:微任务无限循环
function 死循环() {
Promise.resolve().then(死循环);
}
死循环();
(页面直接卡死,因为微任务队列永远清不完)
拓展 : nextTick 的作用
在 Vue 中,当你修改数据后,DOM 并不会立即更新,而是进入一个异步更新队列。nextTick 的作用就是让你在 DOM 更新完成后执行一些操作。
1. 核心思想
- 利用微任务:Vue 会优先使用微任务(如
Promise.then)来调度nextTick回调。 - 降级策略:如果当前环境不支持微任务(如 IE),则降级为宏任务(如
setTimeout)。
2、nextTick 的执行时机
- 当你修改 Vue 实例的数据时,Vue 会将这些更新操作放入一个队列中。
- 在下一个事件循环中,Vue 会清空这个队列并更新 DOM。
nextTick的回调会在 DOM 更新完成后执行。
3、nextTick 与事件循环的关系
微任务优先
- Vue 默认使用微任务(如
Promise.then)来实现nextTick。 - 微任务会在当前事件循环的同步代码执行完毕后立即执行,确保 DOM 更新后回调立即触发。
降级为宏任务
- 在不支持微任务的环境中(如 IE),Vue 会降级为宏任务(如
setTimeout)。 - 宏任务会在下一个事件循环中执行,延迟稍高。
总结
- 利用微任务:优先使用
Promise.then确保回调在 DOM 更新后立即执行。 - 降级策略:在不支持微任务的环境中降级为
setTimeout。 - 与事件循环的关系:
nextTick的回调会在当前事件循环的微任务阶段执行。 - 微任务的优先级高:
指的是 在每一轮事件循环中,微任务会在下一个宏任务之前被强制执行,且必须清空队列。 - 宏任务的优先级低:
必须等待所有微任务执行完毕后,才能执行下一个宏任务。