第一部分:基础概念
1. JavaScript 执行环境
JavaScript 是单线程的,这意味着它一次只能执行一个任务。为了处理异步操作,JavaScript 使用事件循环机制。
2. 核心组件
- 调用栈(Call Stack):执行同步代码的地方
- 任务队列(Task Queue):分为宏任务队列和微任务队列
- 事件循环(Event Loop):协调调用栈和任务队列的机制
第二部分:举例详细解析
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
console.log('script start')asyncfunctionasync1(){awaitasync2()console.log('async1 end')}asyncfunctionasync2(){console.log('async2 end')}async1()setTimeout(function(){console.log('setTimeout')},0)newPromise(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. 事件循环执行顺序
1. 执行一个宏任务(script标签内容) 2. 执行过程中遇到异步任务: - 宏任务 → 放入宏任务队列 - 微任务 → 放入微任务队列 3. 当前宏任务执行完毕 4. 检查微任务队列,依次执行所有微任务 5. 如有必要,进行UI渲染 6. 从宏任务队列取出下一个宏任务执行 7. 回到步骤3,形成循环3. 重要规则
规则1:微任务优先
- 每执行完一个宏任务,都要清空所有微任务
- 微任务执行期间产生的新微任务会加入当前队列,并在本次循环中执行
规则2:async/await 转化
javascript
asyncfunctionexample(){awaitfoo()// 相当于 Promise.resolve(foo()).then(...)console.log('A')// 这部分在微任务队列中}规则3:多个任务队列
- 宏任务可能有多个来源(定时器、I/O等),有各自的队列
- 微任务只有一个队列,按入队顺序执行
在举例理解一下
javascript
// 测试微任务嵌套Promise.resolve().then(()=>{console.log('微任务1');Promise.resolve().then(()=>{console.log('微任务中的微任务');});}).then(()=>{console.log('微任务2');});// 输出顺序:微任务1 → 微任务中的微任务 → 微任务2javascript
// 测试多个宏任务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 的单线程能够处理异步操作