一、JS为什么需要"排队"?
JS 是单线程语言(想象成只有一个收银台的超市),所有任务必须排队执行。但有些任务很耗时(比如网络请求),如果傻等会导致页面卡死,于是有了这样的分工:
// 同步任务:立即执行的普通代码
console.log("我要第一个执行");
// 异步任务:需要等结果的任务
setTimeout(() => {
console.log("我是迟到的小龙虾外卖");
}, 1000);
console.log("我要第二个执行");
输出顺序:
我要第一个执行
我要第二个执行
我是迟到的小龙虾外卖
流程图解:
二、事件循环(Event Loop)是怎么干活的?
事件循环就像个监工,核心工作流程分三步:
- 执行栈:同步代码直接在这里执行(像叠盘子)
- 任务队列:异步任务完成后,回调函数在这里排队(像待取的快递)
- 循环机制:执行栈空了就去队列取任务来执行
经典面试题解析:
console.log("脚本启动");
setTimeout(() => {
console.log("定时器回调");
}, 0);
Promise.resolve().then(() => {
console.log("Promise回调");
});
console.log("脚本结束");
输出顺序:
脚本启动
脚本结束
Promise回调
定时器回调
关键差异:
宏任务(MacroTask) | 微任务(MicroTask) | |
---|---|---|
代表 | setTimeout/setInterval | Promise.then |
优先级 | 低 | 高 |
插入位置 | 任务队列末尾 | 当前执行栈尾部 |
三、为什么微任务能"插队"?
微任务就像 VIP 通道,每次执行栈清空后,必须立即清空所有微任务才会处理宏任务。
代码演示插队现场:
console.log("开始");
// 宏任务(普通顾客)
setTimeout(() => {
console.log("宏任务执行");
}, 0);
// 微任务(VIP 顾客)
Promise.resolve().then(() => {
console.log("微任务1");
}).then(() => {
console.log("微任务2");
});
console.log("结束");
输出顺序:
开始
结束
微任务1
微任务2
宏任务执行
执行流程示意图:
四、解剖 Promise 的执行顺序
Promise 的构造器是同步执行的,但 then/catch 是异步微任务。
分步解析代码:
console.log("1. 同步代码开始");
new Promise((resolve) => {
console.log("2. Promise构造器立即执行");
resolve();
}).then(() => {
console.log("4. 第一个then回调");
}).then(() => {
console.log("5. 第二个then回调");
});
setTimeout(() => {
console.log("6. 定时器回调");
}, 0);
console.log("3. 同步代码结束");
输出顺序:
1. 同步代码开始
2. Promise构造器立即执行
3. 同步代码结束
4. 第一个then回调
5. 第二个then回调
6. 定时器回调
关键点:
new Promise
内部的代码是同步执行的- 每个 then() 都会创建新的微任务
- 微任务链会一次性执行完毕
五、async/await 的本质
async 函数本质上是 Promise 的语法糖,await 会暂停代码执行(但不会阻塞主线程)。
对比代码:
// 传统 Promise 写法
function oldSchool() {
Promise.resolve().then(() => {
console.log("Promise结果");
});
}
// async/await 写法
async function modern() {
await Promise.resolve();
console.log("async结果");
}
oldSchool(); // 输出顺序:Promise结果
modern(); // 输出顺序:async结果
执行流程解析:
async function demo() {
console.log("1. await之前");
await Promise.resolve();
console.log("3. await之后");
}
console.log("0. 外层同步");
demo();
console.log("2. 外层结束");
// 输出顺序:
// 0. 外层同步
// 1. await之前
// 2. 外层结束
// 3. await之后
关键结论:
- await 后面的代码相当于 then() 回调
- 整个 async 函数会返回 Promise 对象
六、完整执行机制流程图解
- 所有同步代码进入执行栈
- 遇到异步任务:
- 宏任务:(如setTimeout/setInterval)交给 WebAPI 计时,完成后回调进入宏任务队列
- 微任务:(如Promise)完成后回调进入微任务队列
- 完成后回调分别进入对应队列
- 执行栈清空后:
- 检查微任务队列,全部执行完毕
- 执行渲染(如果需要)
- 取一个宏任务执行
- 循环往复:
- 每轮循环只执行一个宏任务
- 每个宏任务执行后都会重新检查微任务队列
- 渲染操作通常每秒60次(16.7ms/次),但会受任务阻塞影响
综合练习题:
console.log("start");
setTimeout(() => {
console.log("timeout1");
Promise.resolve().then(() => {
console.log("promise1");
});
}, 0);
setTimeout(() => {
console.log("timeout2");
Promise.resolve().then(() => {
console.log("promise2");
});
}, 0);
Promise.resolve().then(() => {
console.log("promise3");
});
console.log("end");
正确答案:
start
end
promise3
timeout1
promise1
timeout2
promise2
七、核心要点总结
-
执行顺序优先级:
同步代码 > 微任务 > 宏任务 -
常见类型分类:
// 宏任务 setTimeout, setInterval, I/O, UI渲染 // 微任务 Promise.then, process.nextTick, MutationObserver
-
避坑指南:
- 避免在微任务中创建大量微任务导致死循环
- 时间参数为0的定时器不代表立即执行
- 微任务队列优先于页面渲染执行