JavaScript 事件循环机制详解及项目中的应用

第一部分:基础概念

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步:同步任务执行

  1. console.log('1. 同步任务开始') 压入调用栈,立即执行,输出 1
  2. setTimeout 压入调用栈,Web API 开始计时(0ms),回调函数放入宏任务队列
  3. Promise.resolve().then() 压入调用栈,.then() 的回调函数放入微任务队列
  4. console.log('4. 同步任务结束') 压入调用栈,立即执行,输出 4

此时状态:

  • 调用栈:空
  • 微任务队列[Promise.then回调]
  • 宏任务队列[setTimeout回调]

第2步:事件循环检查

  1. 调用栈为空,事件循环开始工作
  2. 优先检查微任务队列,发现有一个任务
  3. 执行微任务:console.log('3. Promise.then 回调'),输出 3
  4. 微任务队列清空

第3步:继续事件循环

  1. 微任务队列为空,现在检查宏任务队列
  2. 执行宏任务:setTimeout 回调,输出 2
  3. 宏任务队列清空

最终输出顺序: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 函数总是返回一个 Promise
  • await 会暂停 async 函数的执行,直到 Promise 解决
  • await 后面的代码相当于放在 .then() 中,属于微任务

执行步骤分析:

第1步:同步任务执行

  1. console.log('script start') → 输出 script start

  2. 定义函数 async1async2(不执行)

  3. 调用 async1()

    • 进入 async1,遇到 await async2()
    • 调用 async2()console.log('async2 end') → 输出 async2 end
    • await 暂停执行,console.log('async1 end') 被包装成微任务放入微任务队列
  4. setTimeout → 回调函数放入宏任务队列

  5. 执行 new Promise

    • console.log('Promise') 是同步代码 → 输出 Promise
    • resolve() 执行,.then() 的回调放入微任务队列

此时状态:

  • 调用栈:空
  • 微任务队列[async1 end, Promise1](注意顺序!)
  • 宏任务队列[setTimeout回调]

第2步:事件循环检查微任务

  1. 调用栈为空,执行微任务

  2. 按入队顺序执行微任务:

    • 第一个微任务:console.log('async1 end') → 输出 async1 end
    • 第二个微任务:console.log('Promise1') → 输出 Promise1
  3. 微任务队列清空

第3步:执行宏任务

  1. 执行 setTimeout 回调 → 输出 setTimeout

最终输出顺序:script start → async2 end → Promise → async1 end → Promise1 → setTimeout

那么到此,应该是可以理解到事件循环的感觉了,那接下来我们就开始看看事件循环的完整逻辑

1. 任务分类

宏任务(Macrotasks)

  • setTimeoutsetInterval
  • setImmediate(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中的微任务;大概就是这种感觉

总结

  1. 同步任务立即执行
  2. 微任务宏任务优先级高
  3. 每个宏任务 执行后,都要清空所有微任务
  4. async/await 本质是 Promise 的语法糖,await 后面的代码是微任务
  5. 事件循环确保了 JavaScript 的单线程能够处理异步操作
相关推荐
bug总结5 小时前
vue+A*算法+canvas解决自动寻路方案
前端·vue.js·算法
王霸天5 小时前
🚀 告别“变形”与“留白”:前端可视化大屏适配的终极方案(附源码)
前端·javascript
LYFlied5 小时前
Vue版本演进:Vue3、Vue2.7与Vue2全面对比
前端·javascript·vue.js
PieroPC5 小时前
Nicegui 组件放在页面中间
前端·后端
Airene5 小时前
Vite 8 发布 beta 版本了,升级体验一下 Rolldown
前端·vite
冰暮流星5 小时前
css3如何引入外部字体
前端·css·css3
ByteCraze5 小时前
从零到一:构建一个实时语音翻译应用(Vue3 + Web Speech API)
前端·开源·github
名字被你们想完了6 小时前
Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(三)
前端·flutter
用户12039112947266 小时前
从零掌握 React JSX:为什么它让前端开发像搭积木一样简单?
前端·react.js·面试