第一部分:基础概念
1. JavaScript 执行环境
JavaScript 是单线程 的,这意味着它一次只能执行一个任务。为了处理异步操作,JavaScript 使用事件循环机制。
2. 核心组件
- 调用栈(Call Stack) :执行同步代码的地方
- 任务队列(Task Queue) :分为宏任务队列和微任务队列
- 事件循环(Event Loop) :协调调用栈和任务队列的机制
第二部分:举例详细解析
js
console.log('1. 同步任务开始');
setTimeout(() => {
console.log('2. setTimeout 回调');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise.then 回调');
});
console.log('4. 同步任务结束');
执行步骤分析:
第1步:同步任务执行
console.log('1. 同步任务开始')压入调用栈,立即执行,输出1setTimeout压入调用栈,Web API 开始计时(0ms),回调函数放入宏任务队列Promise.resolve().then()压入调用栈,.then()的回调函数放入微任务队列console.log('4. 同步任务结束')压入调用栈,立即执行,输出4
此时状态:
- 调用栈:空
- 微任务队列 :
[Promise.then回调] - 宏任务队列 :
[setTimeout回调]
第2步:事件循环检查
- 调用栈为空,事件循环开始工作
- 优先检查微任务队列,发现有一个任务
- 执行微任务:
console.log('3. Promise.then 回调'),输出3 - 微任务队列清空
第3步:继续事件循环
- 微任务队列为空,现在检查宏任务队列
- 执行宏任务:
setTimeout回调,输出2 - 宏任务队列清空
最终输出顺序:1 4 3 2
js
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve =>{
console.log('Promise')
resolve()
}).then(function(){
console.log('Promise1')
})
关键概念:async/await
async函数总是返回一个 Promiseawait会暂停 async 函数的执行,直到 Promise 解决await后面的代码相当于放在.then()中,属于微任务
执行步骤分析:
第1步:同步任务执行
-
console.log('script start')→ 输出script start -
定义函数
async1和async2(不执行) -
调用
async1()- 进入
async1,遇到await async2() - 调用
async2()→console.log('async2 end')→ 输出async2 end await暂停执行,console.log('async1 end')被包装成微任务放入微任务队列
- 进入
-
setTimeout→ 回调函数放入宏任务队列 -
执行
new Promiseconsole.log('Promise')是同步代码 → 输出Promiseresolve()执行,.then()的回调放入微任务队列
此时状态:
- 调用栈:空
- 微任务队列 :
[async1 end, Promise1](注意顺序!) - 宏任务队列 :
[setTimeout回调]
第2步:事件循环检查微任务
-
调用栈为空,执行微任务
-
按入队顺序执行微任务:
- 第一个微任务:
console.log('async1 end')→ 输出async1 end - 第二个微任务:
console.log('Promise1')→ 输出Promise1
- 第一个微任务:
-
微任务队列清空
第3步:执行宏任务
- 执行
setTimeout回调 → 输出setTimeout
最终输出顺序:script start → async2 end → Promise → async1 end → Promise1 → setTimeout
那么到此,应该是可以理解到事件循环的感觉了,那接下来我们就开始看看事件循环的完整逻辑
1. 任务分类
宏任务(Macrotasks)
setTimeout、setIntervalsetImmediate(Node.js)requestAnimationFrame(浏览器)- I/O 操作
- UI 渲染(浏览器)
- 主线程的 script 标签内容
微任务(Microtasks)
Promise.then()、.catch()、.finally()process.nextTick()(Node.js,优先级最高)MutationObserver(浏览器)queueMicrotask()- async/await 的后续代码
2. 事件循环执行顺序
text
1. 执行一个宏任务(script标签内容)
2. 执行过程中遇到异步任务:
- 宏任务 → 放入宏任务队列
- 微任务 → 放入微任务队列
3. 当前宏任务执行完毕
4. 检查微任务队列,依次执行所有微任务
5. 如有必要,进行UI渲染
6. 从宏任务队列取出下一个宏任务执行
7. 回到步骤3,形成循环
3. 重要规则
规则1:微任务优先
- 每执行完一个宏任务,都要清空所有微任务
- 微任务执行期间产生的新微任务会加入当前队列,并在本次循环中执行
规则2:async/await 转化
javascript
js
async function example() {
await foo() // 相当于 Promise.resolve(foo()).then(...)
console.log('A') // 这部分在微任务队列中
}
规则3:多个任务队列
- 宏任务可能有多个来源(定时器、I/O等),有各自的队列
- 微任务只有一个队列,按入队顺序执行
在举例理解一下
javascript
js
// 测试微任务嵌套
Promise.resolve().then(() => {
console.log('微任务1');
Promise.resolve().then(() => {
console.log('微任务中的微任务');
});
}).then(() => {
console.log('微任务2');
});
// 输出顺序:微任务1 → 微任务中的微任务 → 微任务2
javascript
js
// 测试多个宏任务
setTimeout(() => console.log('宏任务1'), 10);
Promise.resolve().then(() => console.log('微任务1'));
setTimeout(() => {
console.log('宏任务2');
Promise.resolve().then(() => console.log('宏任务2中的微任务'));
}, 1);
Promise.resolve().then(() => console.log('微任务2'));
setTimeout(() => {
console.log('宏任务3');
Promise.resolve().then(() => console.log('宏任务3中的微任务'));
}, 0);
结果:
微任务1
微任务2
宏任务3
宏任务3中的微任务
宏任务1
宏任务2
宏任务2中的微任务
为什么呢?聪明的你已经会了
一开始 微任务 放入 微任务1, 微任务2;然后宏任务放入 宏任务3
这个时候, 计时器还没有到底100ms的时候, 打印微任务1、微任务2;然后微任务清空,开始 宏任务3, 放入微任务 宏任务3中的微任务,然后打印宏任务3中的微任务; 然后计时器到了,打印宏任务1、宏任务2
、放入微任务, 打印宏任务2中的微任务;大概就是这种感觉
总结
- 同步任务立即执行
- 微任务 比宏任务优先级高
- 每个宏任务 执行后,都要清空所有微任务
async/await本质是 Promise 的语法糖,await后面的代码是微任务- 事件循环确保了 JavaScript 的单线程能够处理异步操作