Appearance
5. 说一说 js 中的事件循环
难度等级:🟡 中级 分类:JavaScript 归档日期:2026-10-27
其实事件循环应该不属于 JavaScript 语言本身。ECMAScript 规范定义 Job Queue(Promise 那部分),真正的事件循环规则由宿主环境定义。浏览器遵循 HTML 规范,Node 用的是 libuv,他们的行为有些许差异,但思想框架一致。
严格来说,完整的事件循环机制由宿主环境定义。ECMAScript 定义 Promise 等相关的 Job 机制及宿主接口;浏览器的事件循环由 HTML 规范定义,Node.js 则以 libuv 为基础。两者的具体行为有些许不同,但都通过调度任务来协调异步操作。
js 有一个先天的约束,是单线程的。可是他要处理的场景偏偏又充满等待,网络请求,定时器,用户鼠标键盘事件,文件读写等等。那问题就是一个线程,怎么处理那么多不知道什么时候才能完成的事情呢,关键还不能让页面卡住。事件循环就是为了处理这个问题。
通常来说,当前脚本中的同步代码会在调用栈中执行。执行结束,调用栈清空,通常会进行一次微任务队列检查,依次执行微任务,如果期间产生新的微任务,会加入队尾依次执行。
之后,浏览器会根据渲染时机决定是否更新页面,再继续处理后续任务。并不是每一个任务就一定渲染,要看渲染时机。
我们把这样一个过程叫做一个任务,如果在当前任务中,遇到类似定时器等以下宏任务。就把它加入任务队列,等待本次任务(包括同步代码,和微任务)完成,继续执行下一个任务。
常见的宏任务
- 整段
<script>脚本的执行(页面里的第一个任务) setTimeout/setInterval的回调- 用户交互事件:点击、键盘、输入等
- 网络相关回调:XHR 的
onload、fetch底层的网络事件等 postMessage、MessageChannel- Node 中的
setImmediate、I/O 回调
常见的微任务
Promise.then/catch/finally的回调await之后的代码(本质上是 Promise 回调)queueMicrotask():在库或框架代码中做“批量合并”。比如同一个任务内多次修改状态,只在最后统一处理一次,Vue 的nextTick就是类似思路,只是它用的是Promise.then。MutationObserver的回调:监听 DOM 的变化,包括子节点增删、属性修改、文本内容变化- Node 中的
process.nextTick(它有自己的队列,比 Promise 微任务更早执行)。作用:把回调安排在“当前操作完成后、任何其他异步回调之前”执行。
例子
javascript
let pending = false;
const queue = [];
function scheduleUpdate(job) {
queue.push(job);
if (!pending) {
pending = true;
queueMicrotask(flush);
}
}
function flush() {
pending = false;
const jobs = queue.splice(0);
jobs.forEach(job => job());
}javascript
const observer = new MutationObserver((mutations) => {
mutations.forEach(m => console.log(m.type, m));
});
observer.observe(document.body, {
childList: true, // 监听子节点增删
attributes: true, // 监听属性变化
subtree: true, // 包括所有后代节点
});
// 不用时记得断开
observer.disconnect();