目录
JavaScript 是单线程语言,但其通过 事件循环(Event Loop) 和 任务队列(Task Queue) 实现了非阻塞异步执行。
一、同步与异步代码
1. 同步代码(Synchronous Code)
-
特点:
-
顺序执行,阻塞后续代码
-
直接在主线程(调用栈)执行
-
典型场景:普通函数调用、数学运算
console.log('Start'); let sum = 0; for (let i = 0; i < 1e6; i++) sum += i; console.log('End'); // 必须等待循环执行完
-
2. 异步代码(Asynchronous Code)
-
特点:
-
非阻塞执行,后续代码无需等待
-
通过任务队列管理
-
典型场景:
setTimeout
、fetch
、Promise
、DOM事件
console.log('Start'); setTimeout(() => console.log('Timeout'), 0); console.log('End'); // 输出顺序:Start → End → Timeout
-
二、事件循环(Event Loop)
1. 核心组成
组件 | 作用 |
---|---|
调用栈 (Call Stack) | 存放同步执行代码(LIFO结构) |
任务队列 (Task Queue) | 存放待处理的异步任务 |
Web APIs | 浏览器提供的异步API(如DOM、定时器) |
2. 事件循环基本流程
事件循环是 JavaScript 处理异步代码的核心机制,其基本流程如下:
-
执行同步代码
先执行当前调用栈中的所有同步任务(如函数调用、变量赋值)。 -
处理微任务队列
同步代码执行完毕后,立即清空微任务队列(Microtask Queue)中的所有任务(如Promise.then
)。 -
渲染页面(如有需要)
执行 UI 渲染(布局、绘制),但浏览器会智能合并渲染操作以优化性能。 -
处理宏任务队列
从宏任务队列(Macrotask Queue)中取出一个任务执行,回到步骤 1 开始新的事件循环。
3. 运行机制
st=>start: 开始执行
op1=>operation: 执行同步代码
op2=>operation: 遇到异步任务
op3=>operation: 注册到Web APIs
op4=>operation: Web API完成,回调推入任务队列
cond=>condition: 调用栈是否为空?
op5=>operation: 取出队列首个任务推入调用栈
e=>end: 结束
st->op1->op2->op3->op4->cond
cond(yes)->op5->cond
cond(no)->e
三、异步操作分类
1. 任务类型对比
特性 | 宏任务(Macrotask) | 微任务(Microtask) |
---|---|---|
常见类型 | setTimeout 、setInterval 、I/O操作、UI渲染 | Promise.then 、MutationObserver 、process.nextTick (Node.js) |
执行时机 | 每轮事件循环执行一个宏任务 | 当前宏任务执行完毕后立即执行所有微任务 |
优先级 | 低 | 高 |
2. 宏任务(Macrotasks)
-
常见类型:
-
setTimeout
/setInterval
-
I/O 操作(文件读写、网络请求)
-
UI 渲染(浏览器)
-
requestAnimationFrame
(浏览器)
-
-
特点:每次事件循环处理一个宏任务。
-
微任务优先级高于宏任务:每执行完一个宏任务后,会立即清空所有微任务。
3. 微任务(Microtasks)
-
常见类型:
-
Promise.then
/catch
/finally
-
MutationObserver
(浏览器) -
queueMicrotask
-
-
特点:在当前宏任务结束后、下一个宏任务开始前执行所有微任务。
-
微任务可嵌套:若在微任务中生成新的微任务,新微任务会在当前事件循环中被执行。
4. 执行顺序规则
-
执行当前宏任务中的同步代码
-
执行该宏任务产生的所有微任务
-
执行下一个宏任务
-
循环往复(每次循环称为一个"tick")
5. 代码执行顺序示例
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve()
.then(() => console.log('Promise 1'))
.then(() => console.log('Promise 2'));
console.log('End');
输出顺序:
Start
End
Promise 1
Promise 2
Timeout
执行步骤:
-
同步代码依次执行,输出
Start
和End
。 -
setTimeout
回调进入宏任务队列。 -
Promise.then
回调进入微任务队列。 -
同步代码执行完毕,执行所有微任务(
Promise 1
、Promise 2
)。 -
执行下一个宏任务(
Timeout
)。
6. 常见问题
-
为什么微任务优先级高?
微任务通常用于更紧急的更新(如 Promise 状态变更),确保在渲染前完成数据更新,提升用户体验。 -
如何避免微任务饥饿?
避免在微任务中无限递归添加微任务,否则宏任务无法执行,导致页面卡死。 -
requestAnimationFrame
是宏任务还是微任务?
它属于渲染阶段的宏任务,用于在下次重绘前执行动画更新,优先级高于普通宏任务(如setTimeout
)。
四、Promise的同步异步问题
1. 同步执行:Promise 的构造函数和其执行器函数中的代码是同步执行的。
2. 异步回调:.then()
、.catch()
等回调函数是异步的微任务,会在当前同步代码执行完毕后执行。
3. 示例
console.log('1. 同步代码开始');
const promise = new Promise((resolve) => {
console.log('2. Promise 执行器函数(同步)');
resolve('resolve 的值');
});
promise.then((value) => {
console.log('4. .then 回调(异步微任务):', value);
});
console.log('3. 同步代码结束');
// 输出顺序:
// 1. 同步代码开始
// 2. Promise 执行器函数(同步)
// 3. 同步代码结束
// 4. .then 回调(异步微任务): resolve 的值
4. 异步操作的嵌套
console.log('1. 同步代码开始');
const promise = new Promise((resolve) => {
console.log('2. Promise 执行器函数(同步)');
setTimeout(() => {
console.log('5. setTimeout 回调(异步宏任务)');
resolve('resolve 的值');
}, 0);
});
promise.then((value) => {
console.log('6. .then 回调(异步微任务):', value);
});
console.log('3. 同步代码结束');
// 输出顺序:
// 1. 同步代码开始
// 2. Promise 执行器函数(同步)
// 3. 同步代码结束
// 5. setTimeout 回调(异步宏任务)
// 6. .then 回调(异步微任务): resolve 的值
五、async/await 执行顺序
1. 核心规则
-
async
函数
将函数返回值自动包装为 Promise。即使返回非 Promise 值,也会用Promise.resolve()
包装。 -
await
关键字-
暂停当前
async
函数的执行,等待右侧的表达式完成。 -
如果右侧是 Promise,则等待其状态变为 fulfilled 或 rejected。
-
如果右侧是非 Promise 值,则直接将其作为
await
的结果。
-
2. 执行顺序示例
示例代码:
console.log('Start');
async function asyncFunc() {
console.log('Async start');
await Promise.resolve('A');
console.log('Async middle');
await Promise.resolve('B');
console.log('Async end');
}
asyncFunc();
console.log('End');
执行顺序解析:
步骤 | 输出 | 解释 |
---|---|---|
1 | Start | 同步代码优先执行 |
2 | Async start | 调用 asyncFunc() ,执行其内部同步代码 |
3 | End | asyncFunc() 遇到第一个 await ,函数暂停并让出主线程,继续执行外层同步代码 |
4 | Async middle | 第一个 await 的 Promise 完成,恢复执行 asyncFunc() ,输出后遇到第二个 await 再次暂停 |
5 | Async end | 第二个 await 的 Promise 完成,恢复执行并输出 |
最终输出:
Start
Async start
End
Async middle
Async end
3. 执行流程图解
1. 执行同步代码:
- console.log('Start')
- 调用 asyncFunc()
- console.log('Async start')
- 遇到 await,将后续代码包装为微任务
- console.log('End')
2. 主线程清空后,处理微任务队列:
- 执行第一个 await 后的代码:
- console.log('Async middle')
- 遇到第二个 await,再次包装为微任务
3. 再次处理微任务队列:
- 执行第二个 await 后的代码:
- console.log('Async end')
4. 关键原理
1. await
的暂停与恢复机制
-
遇到
await
时,JavaScript 引擎会:-
执行
await
右侧的表达式。 -
暂停
async
函数的执行,将函数内await
之后的代码包装为 微任务,放入微任务队列。 -
继续执行主线程的其他同步代码。
-
2. 微任务优先级
-
微任务(如 Promise 回调、
await
后的代码)在 当前宏任务执行完毕后立即执行,优先级高于宏任务(如setTimeout
)。 -
示例对比:
console.log('Start'); setTimeout(() => console.log('Timeout'), 0); async function test() { await Promise.resolve(); console.log('Microtask'); } test(); console.log('End');
输出顺序:
Start End Microtask // 微任务优先于 setTimeout 宏任务 Timeout
5. 总结
行为 | 执行顺序特点 |
---|---|
同步代码 | 优先执行 |
await 右侧表达式 | 立即执行(若为 Promise,则等待其状态变化) |
await 后的代码 | 包装为微任务,在 当前宏任务结束后、下一个宏任务开始前 执行 |
多个 await | 按顺序执行(除非手动并行化) |
六、Node.js 与浏览器的事件循环差异
特性 | 浏览器 | Node.js |
---|---|---|
微任务类型 | Promise 、MutationObserver | Promise 、process.nextTick |
微任务优先级 | 同层级按注册顺序 | process.nextTick 优先级最高 |
宏任务分层 | 单层任务队列 | 多阶段分层(timers → pending → poll → check → close) |
七、关键总结
-
执行顺序铁律:
同步代码 → 微任务 → 宏任务 → 渲染(浏览器) -
微任务优先:
每个宏任务执行完毕后,必须清空所有微任务队列 -
任务嵌套规则:
-
微任务中产生的微任务会继续在当前批次执行
-
宏任务中产生的任务会进入下一轮循环
-
-
性能优化建议:
-
避免在微任务中进行耗时操作
-
合理分配任务类型(密集计算使用宏任务分片)
-
问答:
什么是事件循环?
答案:执行代码和收集异步任务,在调用栈空闲时,反复调用任务队列里回调函数执行机制。为什么有事件循环?
答案:JavaScript 是单线程的,为了不阻塞 JS 引擎,设计执行代码的模型。JavaScript 内代码如何执行?
答案:执行同步代码,遇到异步代码交给宿主浏览器环境执行 异步有了结果后,把回调函数放入任务队列排队 当调用栈空闲后,反复调用任务队列里的回调函数。什么是宏任务?
答案:浏览器执行的异步代码,例如:JS 执行脚本事件,setTimeout/setInterval,AJAX请求完成事件,用户交互事件等。什么是微任务?
答案:JS 引擎执行的异步代码,例如:Promise对象.then()的回调。
示例
回答下面代码执行顺序:
console.log(1)
setTimeout(() => {
console.log(2)
const p = new Promise(resolve => resolve(3))
p.then(result => console.log(result))
}, 0)
const p = new Promise(resolve => {
setTimeout(() => {
console.log(4)
}, 0)
resolve(5)
})
p.then(result => console.log(result))
const p2 = new Promise(resolve => resolve(6))
p2.then(result => console.log(result))
console.log(7)
输出结果:
1
7
5
6
2
3
4