JS 事件循环(Event loop)机制
✨ 核心概念
-
三大核心结构:
名称 英文 功能定位 主执行栈 Call Stack 存放正在同步执行的函数(同步代码执行区) 宏任务队列 Macrotask Queue 存放需要在主栈空闲后执行的大任务 微任务队列 Microtask Queue 存放在当前宏任务结束前就应立即执行的小任务 -
执行顺序:
- 从宏任务队列中取出一个宏任务(比如
setTimeout的回调),放入主执行栈执行。 - 清空主执行栈中所有 同步代码;
- 清空微任务队列;
- 从宏任务队列取下一个宏任务,重复循环……
- 从宏任务队列中取出一个宏任务(比如
JS 事件循环简述
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
异步实现の浏览器机制
所以浏览器采用异步的方式来避免。 具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。 当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。 ——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 构造函数中的代码是同步执行的!)MuationObserverprocess.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)流程是:
- 执行栈清空 (同步代码): 执行所有同步代码,直到执行栈为空。
- 微任务清空: 检查微任务队列。如果队列不为空,依次取出所有微任务并执行,直到微任务队列清空。
- UI 渲染(浏览器): (在两次宏任务之间)如果有必要,进行一次浏览器渲染。
- 取出宏任务: 检查宏任务队列。取出队列中的第一个宏任务,放入执行栈执行。
- 循环: 重复步骤 1(执行栈清空),开始下一个循环迭代。
Event Loop中,每一次循环称为tick,每一次tick的任务如下:
- 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
- 检查是否存在微任务,有则会执行至微任务队列为空;
- 如果宿主为浏览器,可能会渲染页面;
- 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。
作者:前端香菜君链接:https://juejin.cn/post/7126770198178168840来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。