文章

JS 事件循环(Event loop)机制

✨ 核心概念

  • 三大核心结构:

    名称 英文 功能定位
    主执行栈 Call Stack 存放正在同步执行的函数(同步代码执行区)
    宏任务队列 Macrotask Queue 存放需要在主栈空闲后执行的大任务
    微任务队列 Microtask Queue 存放在当前宏任务结束前就应立即执行的小任务
  • 执行顺序:

    1. 从宏任务队列中取出一个宏任务(比如 setTimeout 的回调),放入主执行栈执行。
    2. 清空主执行栈中所有 同步代码
    3. 清空微任务队列;
    4. 从宏任务队列取下一个宏任务,重复循环……

JS 事件循环简述

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。

在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。

——JS事件循环 - 前端小白栈

异步实现の浏览器机制

所以浏览器采用异步的方式来避免。 具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。 当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。 ——JS事件循环 - 前端小白栈

同步任务进入主线程,即主执行栈,异步任务进入任务队列。主线程内的任务执行完毕为空后,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复,就是事件循环。 ——https://www.yuque.com/xiumubai/doc/qh77zighpkvf5kr3

  • 浏览器最主要的进程有:
    • 浏览器进程:主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。
    • 网络进程:负责加载网络资源。内部会启动多个线程来处理不同的网络任务。
    • 渲染主进程:主线程负责执行 HTML、CSS、JS 代码。

setTimeOut 的延迟机制

我们将结合上面事件循环的知识点,来进行讲解。

setTimeout 的延迟不是“代码暂停”,而是“把回调任务放进事件队列(宏任务队列),等待主执行栈清空后再执行”。 setTimeout 的时间参数只决定它何时被放入任务队列,而不是何时执行。

举个例子:

1
2
3
4
5
6
7
console.log('A')

setTimeout(() => {
  console.log('B')
}, 0)

console.log('C')
  • 当主线程执行到 setTimeout 时,会在调用 setTimeout 的同时,浏览器把它的回调交给 Web API 环境(定时器模块)去计时
  • 直到定时器到期(0ms)时,浏览器/Node 环境发现时间到了,尝试把 () => console.log('B') 加入宏任务队列。
  • 等到主执行栈空闲时,事件循环从宏任务队列中取出回调执行,打印 'B'

宏任务、微任务

  • 宏任务(Macrotask):

    整个主线程执行的基本单位。一个宏任务执行完后,事件循环才会考虑微任务。

  • 微任务(Microtask):

    属于“在当前宏任务结束前立即执行的短任务”,主要由 Promise、MutationObserver 等产生。

1
2
3
4
5
6
7
8
[ 事件循环 ]
   ├─ 宏任务队列(Macrotask Queue)
   │     ├─ setTimeout 回调
   │     ├─ I/O 回调
   │     └─ UI 渲染任务回调
   └─ 微任务队列(Microtask Queue)
         ├─ Promise.then 回调
         └─ queueMicrotask 回调

① 宏任务队列

② 微任务队列

微任务队列(Microtask Queue) 是事件循环中一种高优先级任务队列,用于存放:

  • Promise 的 .then() / .catch() / .finally() 回调
  • queueMicrotask()
  • MutationObserver 等微任务机制触发的任务
  • 执行一个宏任务(例如整段脚本、一次 I/O、一次定时器回调);
  • 执行完这个宏任务后,事件循环会:
    • 检查微任务队列;
    • 把所有微任务(例如 .then() 的回调)全部清空执行
  • 微任务执行完毕后,才进入下一轮宏任务。

✨ 具体举例

面试会有概率让你举出具体哪些函数方法属于宏任务、哪些属于微任务。

  • 宏任务(Macrotasks) 常见的包括:
    • setTimeout()
    • setInterval()
    • setImmediate() (Node.js 独有)
    • I/O 操作
    • UI 渲染
    • postMessage
  • 微任务(Microtasks) 常见的包括:
    • Promise.then().catch().finally() (注意:Promise 构造函数中的代码是同步执行的!)
    • MuationObserver
    • process.nextTick() (Node.js 独有,优先级高于所有微任务)

宏任务 永远排在所有微任务之后执行

微任务等概念の明晰

ES6 规范中,microtask 称为 jobs,macrotask 称为 task 宏任务是由宿主发起的,而微任务由JavaScript自身发起。 ——JS执行顺序——宏任务&微任务

从任务队列具体到宏任务、微任务,尤其是看到微任务的概念,会觉得 “好怪,啥啊” !

我们先明确下 微任务的概念:

当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务。

可以理解为回调事件

既然是 “回调事件” ,那想到 Promise.then() ,就会想:两种任务队列是 “并列” 关系、还是 ”一个宏任务队列中包含一个微任务队列,当前宏任务的微任务队列执行完了、才开始执行下一个宏任务“ 的关系??

浏览器和 Node.js 的事件循环大体上都会维护多个“任务队列”(task queues):

  • 一个或多个 宏任务队列(macrotask queue)
  • 一个 微任务队列(microtask queue)

它们都是“队列”的集合,互相独立,不互相嵌套。

从 Promise 链看微任务

微任务(Microtask)(未详细展开)

https://zh.javascript.info/event-loop

Promise 处理始终是异步的,因为所有 promise 行为都会通过内部的 “promise jobs” 队列,也被称为“微任务队列”(V8 术语)。

一些别的摘抄:

如果一个 promise 的 error 未被在微任务队列的末尾进行处理,则会出现“未处理的 rejection”。

浏览器 VS Node 环境

很复杂,我决定先不细看。此处放一个对比表格。

环境 宏任务(例子) 微任务(例子) 特殊点
浏览器 script 整体、setTimeout、setInterval、requestAnimationFrame、UI 事件 Promise.then、queueMicrotask、MutationObserver 每个宏任务后清空微任务
Node.js setTimeout、setImmediate、I/O 回调、close 事件 Promise.then、process.nextTick nextTick 优先级最高;事件循环有多个阶段

一次事件循环的流程:

完整的事件循环(一次 Loop Tick)流程是:

  1. 执行栈清空 (同步代码): 执行所有同步代码,直到执行栈为空。
  2. 微任务清空: 检查微任务队列。如果队列不为空,依次取出所有微任务并执行,直到微任务队列清空。
  3. UI 渲染(浏览器): (在两次宏任务之间)如果有必要,进行一次浏览器渲染。
  4. 取出宏任务: 检查宏任务队列取出队列中的第一个宏任务,放入执行栈执行。
  5. 循环: 重复步骤 1(执行栈清空),开始下一个循环迭代。

Event Loop中,每一次循环称为tick,每一次tick的任务如下:

  • 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
  • 检查是否存在微任务,有则会执行至微任务队列为空
  • 如果宿主为浏览器,可能会渲染页面;
  • 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。

作者:前端香菜君链接:https://juejin.cn/post/7126770198178168840来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文由作者按照 CC BY 4.0 进行授权