Chrome中的Event Loop

23 年 7 月 21 日 星期五 (已编辑)
1506 字
8 分钟

Event Loop(事件循环)是JavaScript实现异步编程的核心机制。它负责协调和管理JavaScript代码的执行、DOM更新、用户交互等各种事件。

Engine.png

image from Phillip Robert’s JSConfEU talk “What the heck is the event loop anyway?

先理解为什么需要事件循环 🤔

想象你在餐厅点餐:

服务员(主线程)不能同时做多件事,但是餐厅需要同时处理:

  • 接待新客人
  • 点餐
  • 上菜
  • 结账
  • 清理餐桌

这就像浏览器需要同时处理:

  • 用户点击
  • 处理网络请求
  • 更新页面显示
  • 执行 JavaScript 代码

JavaScript 就像一个独自工作的服务员,一次只能做一件事。但是通过事件循环的机制,可以让它看起来像是在同时处理多件事。

the-Node-js-event-loop.png

the Nodejs event loop

Event Loop 的执行顺序

Event Loop

图片来自JSConf.Asia-新加坡國會大廈劇院-2018年1月27日 https://2018.jsconf.asia

根据上图,Event Loop 的执行过程可以分为以下几个关键阶段:

  1. Task Queues (任务队列)
  • Event Loop在循环中处理分配的任务
  1. requestAnimationFrame (rAF)
  • 在每一帧开始时执行
  • 用于处理动画相关的操作
  • 保证动画流畅运行
  1. 渲染管线阶段(The Render steps)
  • Style (样式计算),计算元素的最终样式

  • Layout (布局),计算元素的位置和大小

  • Paint (绘制),将元素绘制到屏幕上

执行流程示例

javascript
console.log('script start'); // 宏任务

setTimeout(() => {
  console.log('setTimeout'); // 下一个宏任务
}, 0);

requestAnimationFrame(() => {
  console.log('rAF'); // 在下一帧执行
});

Promise.resolve().then(() => {
  console.log('promise'); // 微任务
});

console.log('script end'); // 当前宏任务

输出顺序:

plaintext
script start
script end
promise
setTimeout
rAF

代码执行分析

第一轮循环(当前宏任务)

  1. 执行同步代码
javascript
console.log('script start'); // 立即执行
console.log('script end'); // 立即执行
  1. 任务分配

  2. setTimeout 回调被放入宏任务队列

  3. Promise.then 回调被放入微任务队列

  4. requestAnimationFrame 回调被放入 rAF 队列

  5. 检查微任务队列

javascript
console.log('promise'); // 执行微任务

第二轮循环(下一个宏任务)

  1. 执行 setTimeout 回调
javascript
console.log('setTimeout');

最后执行 rAF

  1. 等待浏览器下一帧
javascript
console.log('rAF');

需要注意的是:

  1. setTimeout 的延迟
  • 即使设置为 0,也有最小延迟时间,4.7ms为默认值
  • 实际执行时间可能比预期要晚
  1. 执行优先级
  • 宏任务 > requestAnimationFrame > 渲染管线
  • 微任务总是在当前宏任务结束后立即执行,优先级高于下一个宏任务
  1. 渲染时机
  • 不是每轮 Event Loop 都会触发渲染
  • 浏览器会根据屏幕刷新率和性能来决定是否渲染

宏任务队列、RAF队列和微任务队列的对比

宏任务队列(Macrotask Queue)

执行时机

  • 按照队列顺序依次执行
  • 每次事件循环只执行一个
  • 执行完后会检查微任务队列
javascript
// 多个宏任务按序执行
setTimeout(() => console.log(1), 0);
setTimeout(() => console.log(2), 0);
setTimeout(() => console.log(3), 0);
// 每个都是独立的宏任务

常见宏任务源

js
// 1. script(整体代码)
console.log('这是一个宏任务');

// 2. setTimeout
setTimeout(() => {
  console.log('setTimeout 宏任务');
}, 0);

// 3. setInterval
setInterval(() => {
  console.log('setInterval 宏任务');
}, 1000);

// 4. setImmediate (Node.js环境)
setImmediate(() => {
  console.log('setImmediate 宏任务');
});

// 5. I/O操作
fs.readFile('file.txt', () => {
  console.log('I/O 宏任务');
});

// 6. UI渲染
// 7. MessageChannel
// 8. postMessage

微任务队列(Microtask Queue)

执行时机

  • 当前宏任务执行完立即执行
  • 清空整个微任务队列,中途追加的微任务也会执行
  • 优先于RAF和UI渲染
javascript
// 微任务可以添加新的微任务
Promise.resolve().then(() => {
  console.log('Micro 1');
  Promise.resolve().then(() => {
    console.log('Micro 2'); // 同一轮循环执行
  });
});

常见的微任务源

js
// 1. Promise的then/catch/finally回调
Promise.resolve().then(() => {
  console.log('Promise.then 微任务');
});

// 2. async/await(实际上是Promise的语法糖)
async function example() {
  await Promise.resolve();
  console.log('async/await 微任务');
}

// 3. process.nextTick (Node.js环境)
process.nextTick(() => {
  console.log('nextTick 微任务');
});

// 4. MutationObserver(监听DOM变化)
const observer = new MutationObserver(() => {
  console.log('MutationObserver 微任务');
});

注意点

js
// process.nextTick 优先级高于 Promise.then
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// 输出: nextTick -> promise

// 所有微任务都会在下一个宏任务之前执行完
Promise.resolve().then(() => {
  console.log('1');
  Promise.resolve().then(() => console.log('2'));
});

RAF队列(RequestAnimationFrame Callbacks)

执行特征

  • 与显示器刷新率同步(通常是60fps)
  • 在UI每帧渲染前执行,多余的任务会安排到下一帧
  • 适合处理动画相关操作
javascript
// RAF与帧同步
requestAnimationFrame(() => {
  console.log('Frame 1');
  requestAnimationFrame(() => {
    console.log('Frame 2'); // 下一帧执行
  });
});

在事件监听器中

有以下代码

js
// 第一个点击监听器
button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('Microtask 1')); // 微任务
  console.log('Listener 1'); // 同步代码
});

// 第二个点击监听器
button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('Microtask 2')); // 微任务
  console.log('Listener 2'); // 同步代码
});

当用户点击按钮时,输出顺序将是:

js
Listener 1
Microtask 1
Listener 2
Microtask 2

执行顺序解析

  • 首先执行第一个监听器中的同步代码 console.log('Listener 1'),执行后堆栈为空,因为监听器已经执行完毕
  • 此时执行微任务队列,即第一个Promise的回调 console.log('Microtask 1')
  • 然后执行第二个监听器中的同步代码 console.log('Listener 2')
  • 同理执行第二个Promise的回调 console.log('Microtask 2')

特殊情况

不是用户点击,而是通过JS触发时:

js
button.click(); // 同步调用

输出顺序将是:

js
Listener 1
Listener 2
Microtask 1
Microtask 2

button.click() 是同步调用,在当前执行栈中直接执行,会立即执行所有注册的事件监听器。而用户手动点击是异步触发,会创建新的宏任务,所有同步代码(包括click()调用)执行完后,才会执行微任务队列。

执行顺序解析

  • click调用,button同步分发事件,事件监听器按照注册顺序依次执行
  • 首先执行第一个监听器中的同步代码 console.log('Listener 1'),执行完后,注意此时堆栈不为空,click还未执行完毕。
  • 接着是第二个监听器中的同步代码 console.log('Listener 2')
  • 然后是微任务队列的清空,Microtask 1 和 Microtask 2

参考资料

What is the JavaScript Event Loop?

文章标题:Chrome中的Event Loop

文章作者:shirtiny

文章链接:https://kizamu.anror.com/posts/event-loop[复制]

最后修改时间:


商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文采用CC BY-NC-SA 4.0进行许可。