网站

并发模型与事件循环 - JavaScript | MDN

setTimeout - JavaScript | MDN

引入

JS 执行机制 里学过,异步回调会放到任务队列里排队。

但任务队列其实不止一个。

JS 里有宏任务微任务两种队列,它们的执行优先级不同,决定了异步代码的执行顺序。

正文

定义

定义

事件循环中,宏任务(Macro Task)和微任务(Micro Task)是两类不同的异步任务。每次事件循环会先执行一个宏任务,然后把所有微任务清空,再进入下一轮。

宏任务

宏任务是常见的异步操作,包括:

  • setTimeout
  • setInterval
  • I/O 操作(网络请求、文件读写)
  • UI 渲染(页面更新)
  • setImmediate(Node.js)
setTimeout(function () {
  console.log("宏任务:setTimeout");
}, 0);
 
setInterval(function () {
  console.log("宏任务:setInterval");
}, 1000);

每个宏任务执行完后,浏览器可能会进行 UI 渲染。

微任务

微任务的优先级比宏任务高,包括:

  • Promise.thenPromise.catchPromise.finally
  • MutationObserver(DOM 变化监听)
  • queueMicrotask
  • process.nextTick(Node.js)
Promise.resolve().then(function () {
  console.log("微任务:Promise.then");
});

执行顺序

一轮事件循环的执行顺序:

  1. 执行当前同步代码(宏任务的一部分)
  2. 同步代码执行完后,清空所有微任务
  3. 微任务清空后,可能进行 UI 渲染
  4. 取下一个宏任务执行
  5. 回到第 2 步

用一个表来总结:

类型常见任务优先级
同步代码普通语句最先执行
微任务Promise.thenMutationObserver同步之后、宏任务之前
宏任务setTimeoutsetInterval、UI 渲染微任务之后

基础例子

console.log("1 - 同步");
 
setTimeout(function () {
  console.log("2 - 宏任务");
}, 0);
 
Promise.resolve().then(function () {
  console.log("3 - 微任务");
});
 
console.log("4 - 同步");

执行过程:

  1. console.log("1 - 同步") — 同步代码,直接执行
  2. setTimeout — 回调放入宏任务队列
  3. Promise.resolve().then() — 回调放入微任务队列
  4. console.log("4 - 同步") — 同步代码,直接执行
  5. 同步代码完成,清空微任务队列,执行 Promise.then 回调
  6. 微任务清空,取宏任务队列中的 setTimeout 回调执行

输出:

1 - 同步
4 - 同步
3 - 微任务
2 - 宏任务

微任务可以插入更多微任务

微任务执行过程中产生的新微任务,会在本轮就执行完。

Promise.resolve()
  .then(function () {
    console.log("微任务 1");
    return Promise.resolve();
  })
  .then(function () {
    console.log("微任务 2");
  });
 
setTimeout(function () {
  console.log("宏任务");
}, 0);

输出:

微任务 1
微任务 2
宏任务

微任务链式调用产生的新微任务,全部在当前轮清空,不会等到下一轮。

宏任务之间是独立的

每个宏任务执行完后,才会去清空微任务队列。

setTimeout(function () {
  console.log("宏任务 1");
  Promise.resolve().then(function () {
    console.log("宏任务 1 的微任务");
  });
}, 0);
 
setTimeout(function () {
  console.log("宏任务 2");
}, 0);

输出:

宏任务 1
宏任务 1 的微任务
宏任务 2

第一个宏任务执行时产生的微任务,会在第二个宏任务开始前清空。

综合题目

这道题包含了同步、微任务、宏任务的混合:

console.log("开始");
 
setTimeout(function () {
  console.log("setTimeout 1");
}, 0);
 
Promise.resolve()
  .then(function () {
    console.log("Promise 1");
  })
  .then(function () {
    console.log("Promise 2");
  });
 
setTimeout(function () {
  console.log("setTimeout 2");
}, 0);
 
console.log("结束");

逐步分析:

  1. console.log("开始") — 同步,直接输出
  2. setTimeout 回调 1 — 放入宏任务队列
  3. Promise.then 第一个 — 放入微任务队列
  4. setTimeout 回调 2 — 放入宏任务队列
  5. console.log("结束") — 同步,直接输出
  6. 同步完成,清空微任务:输出 Promise 1,再输出 Promise 2
  7. 取宏任务 1:输出 setTimeout 1
  8. 取宏任务 2:输出 setTimeout 2

输出:

开始
结束
Promise 1
Promise 2
setTimeout 1
setTimeout 2

带 async/await 的题目

async 函数中 await 后面的代码相当于放到微任务里。

async function test() {
  console.log("async 1");
  await Promise.resolve();
  console.log("async 2");
}
 
console.log("同步 1");
 
test();
 
console.log("同步 2");

执行过程:

  1. console.log("同步 1") — 同步输出
  2. 调用 test(),进入函数体
  3. console.log("async 1") — 同步输出
  4. await — 后面的代码相当于微任务
  5. 回到外部,console.log("同步 2") — 同步输出
  6. 同步完成,执行微任务:输出 async 2

输出:

同步 1
async 1
同步 2
async 2

面试常见套路

套路说明
setTimeout(fn, 0)Promise.then 比较微任务先于宏任务
async 函数中 await 前后的代码await 前是同步,后面是微任务
多个 setTimeout 和多个 Promise微任务全部清完才轮到下一个宏任务
宏任务中产生微任务当前宏任务结束后、下一个宏任务开始前清空
微任务中产生微任务本轮就执行完,不会拖到下一轮

特点

  • 同步代码最先执行
  • 微任务优先级高于宏任务
  • 每轮事件循环:一个宏任务 → 所有微任务 → 可能 UI 渲染 → 下一个宏任务
  • 微任务中新增的微任务在本轮就执行
  • setTimeout(fn, 0) 不等于立即执行,要等当前同步和微任务都完成

理解

把事件循环理解成餐厅的点单系统

同步代码是厨师手上正在做的菜,必须先做完。

微任务是 VIP 订单,厨师做完手上的菜,优先处理所有 VIP 订单,包括 VIP 订单追加的甜品。

宏任务是普通订单,VIP 订单全部处理完,才按顺序做普通订单。

每做完一个普通订单,就检查一下有没有新的 VIP 订单进来。

同步 > 微任务 > 宏任务,这就是 JS 异步代码的执行优先级。

引出

理解了事件循环之后,可以深入学习 JS Promise,了解微任务最常用的载体。

也可以学习 JS 异步编程,了解从回调到 Promise 再到 async/await 的演变过程。

还可以结合 JS 定时器,理解 setTimeoutsetInterval 在事件循环中的表现。