JavaScript 事件循环、宏任务与微任务详解与进阶解析

JavaScript 的单线程特性决定了其异步编程模型的核心——事件循环(Event Loop)。理解事件循环、宏任务(MacroTask)和微任务(MicroTask)的机制,是掌握前端异步编程的关键。本文将从基础概念到进阶场景,结合案例详细解析这一机制。


在这里插入图片描述

一、事件循环基础

1. 为什么需要事件循环?

JavaScript 是单线程语言,同步代码会阻塞主线程,导致页面无响应。例如:

console.log("Start");
while (true) {} // 死循环,页面卡死
console.log("End"); // 永远不会执行

事件循环通过将异步任务放入任务队列,避免主线程阻塞,实现“非阻塞”效果。

2. 事件循环的核心机制

  • 执行栈(Call Stack):同步代码按顺序执行,形成调用栈。
  • 任务队列(Task Queue):异步任务完成后,回调函数进入任务队列。
  • 事件循环:主线程执行栈清空后,从任务队列中取出任务执行。

二、宏任务与微任务

1. 宏任务(MacroTask)

  • 定义:由宿主环境(浏览器/Node.js)提供的异步任务,每次执行一个宏任务。
  • 常见宏任务
    • setTimeout / setInterval
    • I/O 操作(如文件读写)
    • UI 渲染
    • 事件监听(如 click
    • setImmediate(Node.js 特有)
    • script(整体代码)

2. 微任务(MicroTask)

  • 定义:在当前宏任务执行完毕后、下一个宏任务执行前执行的异步任务。
  • 常见微任务
    • Promise.then / catch / finally
    • MutationObserver(监听 DOM 变化)
    • queueMicrotask(现代浏览器原生 API)
    • process.nextTick(Node.js 特有,优先级最高)

3. 执行顺序规则

  1. 执行同步代码(宏任务)。
  2. 执行所有微任务。
  3. 渲染页面(浏览器环境)。
  4. 执行下一个宏任务。
  5. 重复上述步骤。

三、案例解析

案例 1:基础执行顺序

console.log("Start");

setTimeout(() => {
  console.log("setTimeout");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise 1");
}).then(() => {
  console.log("Promise 2");
});

console.log("End");

输出

Start
End
Promise 1
Promise 2
setTimeout

解析

  1. 同步代码 StartEnd 执行。
  2. 微任务队列:Promise 1Promise 2,依次执行。
  3. 宏任务队列:setTimeout 执行。

案例 2:微任务嵌套

console.log("Start");

setTimeout(() => {
  console.log("setTimeout 1");
  Promise.resolve().then(() => {
    console.log("Promise in setTimeout");
  });
}, 0);

Promise.resolve().then(() => {
  console.log("Promise 1");
  setTimeout(() => {
    console.log("setTimeout 2");
  }, 0);
}).then(() => {
  console.log("Promise 2");
});

console.log("End");

输出

Start
End
Promise 1
Promise 2
setTimeout 1
Promise in setTimeout
setTimeout 2

解析

  1. 同步代码 StartEnd 执行。
  2. 微任务队列:
    • Promise 1Promise 2 执行。
    • Promise 1setTimeout 2 进入宏任务队列。
  3. 宏任务队列:
    • setTimeout 1 执行,内部 Promise in setTimeout 进入微任务队列。
    • setTimeout 2 执行。

案例 3:process.nextTick(Node.js 环境)

console.log("Start");

setTimeout(() => {
  console.log("setTimeout");
}, 0);

process.nextTick(() => {
  console.log("nextTick 1");
  process.nextTick(() => {
    console.log("nextTick 2");
  });
});

Promise.resolve().then(() => {
  console.log("Promise");
});

console.log("End");

输出(Node.js)

Start
End
nextTick 1
nextTick 2
Promise
setTimeout

解析

  1. process.nextTick 的优先级高于微任务,在当前宏任务结束后立即执行。
  2. 嵌套的 nextTick 2 会在当前 nextTick 阶段完成后执行。

四、进阶详解

1. 浏览器与 Node.js 的差异

  • 浏览器
    • 微任务:Promise.thenMutationObserver
    • 宏任务:setTimeoutI/OUI 渲染
  • Node.js
    • 微任务:Promise.thenqueueMicrotaskprocess.nextTick
    • 宏任务:setTimeoutsetImmediateI/O
    • process.nextTick 优先级高于其他微任务。

2. 避免常见问题

  • 死循环:同步代码中的死循环会阻塞事件循环。
  • 微任务滥用:在微任务中执行大量操作会导致页面渲染延迟。
  • setImmediate vs setTimeout(fn, 0)
    • setImmediate:在当前事件循环的下一个阶段执行。
    • setTimeout(fn, 0):在下一个事件循环的宏任务阶段执行。

3. 优化建议

  • 使用 async/await:简化异步代码,避免回调地狱。
  • 合理使用微任务:将高优先级的任务(如 DOM 更新)放入微任务。
  • 避免同步阻塞:将耗时操作(如复杂计算)放入 Web Worker。

五、总结

  1. 事件循环是 JavaScript 异步编程的核心,通过宏任务和微任务实现非阻塞。
  2. 执行顺序:同步代码 → 微任务 → 渲染 → 宏任务。
  3. 微任务优先级高于宏任务process.nextTick(Node.js)优先级最高。
  4. 合理使用异步机制,避免阻塞主线程,提升页面性能。

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二川bro

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值