Web前端入门第 68 问:JavaScript 事件循环机制中的微任务与宏任务

JS 是单线程语言。这句话对不对?

按照目前的情况来看,JS 自从支持了 Web Worker 之后,就不再是单线程语言了,但 Worker 的工作线程与主线程有区别,在 Worker 的工作线程中无法直接操作 DOM、window 对象或大多数浏览器 API(如 localStorage),Worker 的全局对象也不再是 window 对象,而是 self。

Worker 中的事件循环与主线程相互独立,互不影响,但执行顺序还是得遵循 JS 的语法规则。

宏任务

宏任务表示执行时间较长的任务,在每次时间循环时只会执行一个宏任务,执行完毕后处理微任务队列,所有微任务都执行完毕后进入下一个宏任务。

JS 常见宏任务类型:

  1. 定时器任务:setTimeout / setInterval
  2. DOM 事件回调(如 click、scroll)
  3. I/O 操作(如文件读取、网络请求)
  4. 浏览器用于执行动画的方法 requestAnimationFrame ,执行时机与渲染相关
  5. Node.js 环境的 setImmediate
  6. script 标签内主线程的同步代码(整体作为一个宏任务)

微任务

微任务表示更轻量的异步任务,当宏任务执行完毕之后立即执行。

JS 常见微任务类型:

  1. Promise.then() / Promise.catch() / Promise.finally()
  2. 浏览器监听 DOM 变化的 API 对象,比如:MutationObserver
  3. 手动添加微任务API方法:queueMicrotask()
  4. nodejs 中的 process.nextTick()

代码解析

看这么一段代码:

js 复制代码
(function() {
  console.log(1)
  setTimeout(() => { console.log(2); });
  queueMicrotask(() => console.log(3))
  new Promise(resolve => {
    console.log(4);
    setTimeout(() => {
      resolve();
      console.log(5);
    }, 0);
    Promise.resolve().then(() => console.log(6));
    console.log(7);
  }).then(() => {
    console.log(8);
    Promise.resolve().then(() => console.log(9));
  });
  console.log(10);
})();

分析代码:

js 复制代码
(function() {
  console.log(1) // 同步任务
  setTimeout(() => { console.log(2); });
  queueMicrotask(() => console.log(3))
  new Promise(resolve => {
    console.log(4); // 同步任务
    setTimeout(() => { // 宏任务
      resolve(); // 宏任务的同步任务
      console.log(5); // 宏任务中的同步任务
    }, 0);
    Promise.resolve().then(() => console.log(6)); // 微任务
    console.log(7); // 同步任务
  }).then(() => { // 微任务
    console.log(8); // 微任务中的同步任务
    Promise.resolve().then(() => console.log(9)); // 微任务中的微任务
  });
  console.log(10); // 同步任务
})();

第一轮

首先同步代码的宏任务优先级最高,不管微任务还是宏任务,同步代码都会先执行。

所以上面代码会优先执行:

js 复制代码
console.log(1)
console.log(4);
console.log(7);
console.log(10);

接着开始处理微任务:

js 复制代码
queueMicrotask(() => console.log(3))
Promise.resolve().then(() => console.log(6));

微任务处理完,开始执行下一轮宏任务。

第二轮

这一轮中的宏任务只有一个 setTimeout,执行完之后由于没有微任务队列,所以直接执行下一轮宏任务。

js 复制代码
setTimeout(() => { console.log(2); });

第三轮

这一轮的宏任务中有同步代码。

js 复制代码
setTimeout(() => { // 宏任务
  resolve(); // 宏任务的同步任务
  console.log(5); // 宏任务中的同步任务
}, 0);

在执行完 resolve() 之后,会将 Promise.then 的回调函数放入微任务队列中,所以在宏任务执行完之后会开始微任务:

js 复制代码
then(() => { // 微任务
  console.log(8); // 微任务中的同步任务
  Promise.resolve().then(() => console.log(9)); // 微任务中的微任务
})

最终的打印顺序

bash 复制代码
1
4
7
10
3
6
2
5
8
9

执行流程图

JS 代码逐行执行,在遇到宏任务时,整个代码块丢到宏任务队列,在遇到微任务时,将微任务丢到本次事件循环中的微任务队列,本次事件循环执行完之后,再执行微任务队列中的任务,微任务执行完之后开始下一个宏任务执行。

JS 代码执行机制:

宏任务执行机制:

写在最后

JS 中的代码执行流程永远都是事件循环机制,这是 JS 的任务调度核心,理解事件循环机制,才能在开发中游刃有余~~