JavaScript事件循环

目录

一、同步与异步代码

1. 同步代码(Synchronous Code)

2. 异步代码(Asynchronous Code)

二、事件循环(Event Loop)

1. 核心组成

2. 事件循环基本流程

3. 运行机制

三、异步操作分类

1. 任务类型对比

2. 宏任务(Macrotasks)

3. 微任务(Microtasks)

4. 执行顺序规则

5. 代码执行顺序示例

6. 常见问题

四、Promise的同步异步问题

五、async/await 执行顺序

1. 核心规则

2. 执行顺序示例

3. 执行流程图解

4. 关键原理

5. 总结

六、Node.js 与浏览器的事件循环差异

七、关键总结

示例


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)

  • 特点

    • 非阻塞执行,后续代码无需等待

    • 通过任务队列管理

    • 典型场景:setTimeoutfetchPromiseDOM事件

    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 处理异步代码的核心机制,其基本流程如下:

  1. 执行同步代码
    先执行当前调用栈中的所有同步任务(如函数调用、变量赋值)。

  2. 处理微任务队列
    同步代码执行完毕后,立即清空微任务队列(Microtask Queue)中的所有任务(如 Promise.then)。

  3. 渲染页面(如有需要)
    执行 UI 渲染(布局、绘制),但浏览器会智能合并渲染操作以优化性能。

  4. 处理宏任务队列
    宏任务队列(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)
常见类型setTimeoutsetInterval、I/O操作、UI渲染Promise.thenMutationObserverprocess.nextTick(Node.js)
执行时机每轮事件循环执行一个宏任务当前宏任务执行完毕后立即执行所有微任务
优先级

2. 宏任务(Macrotasks)

  • 常见类型

    • setTimeout / setInterval

    • I/O 操作(文件读写、网络请求)

    • UI 渲染(浏览器)

    • requestAnimationFrame(浏览器)

  • 特点:每次事件循环处理一个宏任务。

  • 微任务优先级高于宏任务:每执行完一个宏任务后,会立即清空所有微任务。

3. 微任务(Microtasks)

  • 常见类型

    • Promise.then / catch / finally

    • MutationObserver(浏览器)

    • queueMicrotask

  • 特点:在当前宏任务结束后、下一个宏任务开始前执行所有微任务。

  • 微任务可嵌套:若在微任务中生成新的微任务,新微任务会在当前事件循环中被执行。

4. 执行顺序规则

  1. 执行当前宏任务中的同步代码

  2. 执行该宏任务产生的所有微任务

  3. 执行下一个宏任务

  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

执行步骤

  1. 同步代码依次执行,输出 Start 和 End

  2. setTimeout 回调进入宏任务队列。

  3. Promise.then 回调进入微任务队列。

  4. 同步代码执行完毕,执行所有微任务(Promise 1Promise 2)。

  5. 执行下一个宏任务(Timeout)。

6. 常见问题

  1. 为什么微任务优先级高?
    微任务通常用于更紧急的更新(如 Promise 状态变更),确保在渲染前完成数据更新,提升用户体验。

  2. 如何避免微任务饥饿?
    避免在微任务中无限递归添加微任务,否则宏任务无法执行,导致页面卡死。

  3. 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. 核心规则

  1. async 函数
    将函数返回值自动包装为 Promise。即使返回非 Promise 值,也会用 Promise.resolve() 包装。

  2. 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');

执行顺序解析:

步骤输出解释
1Start同步代码优先执行
2Async start调用 asyncFunc(),执行其内部同步代码
3EndasyncFunc() 遇到第一个 await,函数暂停并让出主线程,继续执行外层同步代码
4Async middle第一个 await 的 Promise 完成,恢复执行 asyncFunc(),输出后遇到第二个 await 再次暂停
5Async 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 引擎会:

    1. 执行 await 右侧的表达式。

    2. 暂停 async 函数的执行,将函数内 await 之后的代码包装为 微任务,放入微任务队列。

    3. 继续执行主线程的其他同步代码。

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
微任务类型PromiseMutationObserverPromiseprocess.nextTick
微任务优先级同层级按注册顺序process.nextTick 优先级最高
宏任务分层单层任务队列多阶段分层(timers → pending → poll → check → close)

七、关键总结

  1. 执行顺序铁律
    同步代码 → 微任务 → 宏任务 → 渲染(浏览器)

  2. 微任务优先
    每个宏任务执行完毕后,必须清空所有微任务队列

  3. 任务嵌套规则

    • 微任务中产生的微任务会继续在当前批次执行

    • 宏任务中产生的任务会进入下一轮循环

  4. 性能优化建议

    • 避免在微任务中进行耗时操作

    • 合理分配任务类型(密集计算使用宏任务分片)


问答:

  1. 什么是事件循环?

    ​答案​:执行代码和收集异步任务,在调用栈空闲时,反复调用任务队列里回调函数执行机制。
  2. 为什么有事件循环?

    ​答案​:JavaScript 是单线程的,为了不阻塞 JS 引擎,设计执行代码的模型。
  3. JavaScript 内代码如何执行?

    ​答案​:执行同步代码,遇到异步代码交给宿主浏览器环境执行 异步有了结果后,把回调函数放入任务队列排队 当调用栈空闲后,反复调用任务队列里的回调函数。
  4. 什么是宏任务?

    ​答案​:浏览器执行的异步代码,例如:JS 执行脚本事件,setTimeout/setInterval,AJAX请求完成事件,用户交互事件等。
  5. 什么是微任务?

    ​答案​: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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值