LINNIL

事件循环 Event Loop

Lucas

2024年2月10日
事件循环 Event Loop

基本概念

事件循环(Event Loop)是 JavaScript 运行时环境中负责管理异步任务执行的一种机制。在浏览器中, 事件循环是由浏览器的 JavaScript 引擎(如 V8 引擎)负责实现;在 Node.js 等环境中,也有自己的事件循环实现。 简单来说,事件循环就是轮询任务队列,执行任务,休眠的无限循环。

事件循环的意义

原因很简单,JavaScript 是单线程语言,单线程意味着同一时间只能执行一个任务,为了能有效处理异步任务,而不会阻塞主线程,就需要并发处理, 而事件循环就是并发的一种形式。这对于交互式的 Web 应用和处理网络请求等操作至关重要。

任务队列

任务队列是事件循环的核心组成部分,用于管理和调度异步任务的执行顺序。当任务到来时,JavaScript 引擎可能处于忙碌状态,那么任务会被排入队列里。

eventloop.png

队列中的任务遵循 “ 先进先出 ” 的原则。当浏览器引擎执行完 script 后,它会处理 setTimeout 事件,然后处理 I/O 事件,以此类推。

Tips: 在执行任务时不会进行渲染操作,仅在任务完成后才会进行渲染; 任务花费的时间过长,浏览器将无法执行其他任务。会抛出一个 “页面未响应” 的警报。这种情况常发生在有大量复杂的计算或死循环的程序错误时。

因为队列 “先进先出” 的原则,所以引入微任务队列来执行高优先级的任务。通过微任务可以让开发者更精确地控制异步操作的执行顺序和时机。

在每个宏任务执行之后,引擎会执行微任务队列中的 所有 任务,然后再执行其他宏任务。

Example:

setTimeout(() => console.log(1));

Promise.resolve().then(() => console.log(2));

console.log(3);
// 输出结果:3 2 1
  1. 首先输出 3 因为是常规同步调用;
  2. 输出 2,then 回调函数会被放入微任务队列中,并在当前代码结束后执行;
  3. 输出 1,timeout 属于宏任务。
eventloop2.png

引入微任务之后,事件循环算法优化为:

  1. 从宏任务队列中出队并执行任务。
  2. 查看微任务队列查看是否有任务:
    1. 当微任务队列非空时,执行微任务。
    2. 当微任务队列为空时,返回宏任务队列。
  3. 如果有 dom 变更,则进行渲染。
  4. 如果宏任务队列为空,休眠等待。
  5. 转到步骤 1。

以上步骤非完整步骤,完整事件循环可以了解: 事件循环

微任务会在执行任何其他事件处理,或渲染,或执行任何其他宏任务之前完成。这确保了微任务之间的应用程序环境基本相同(没有鼠标坐标更改, 没有新的网络数据等)。

常见的宏任务:

  • <script>
  • setTimeout
  • setInterval
  • requestAnimationFrame
  • I/O 操作等

常见的微任务:

  • Promise 的 resolve 和 reject 回调(then()
  • MutationObserver 的回调
  • process.nextTick (Node.js 环境)

总结

  • 事件循环是用于处理异步任务的机制。负责管理和调度异步任务的执行顺序,确保它们在适当的时机被执行,并且不会阻塞主线程。
  • 异步任务包括宏任务(Macro Task)和微任务(Micro Task)。宏任务通常由浏览器的 Web API 提供。微任务通常通过 Promise 的回调触发。 例如通过 setTimeout(fn, 0) 添加宏任务,通过 queueMicrotask(fn) 添加微任务。
  • 微任务的优先级会高于宏任务,当前宏任务执行完毕后立即执行微任务,并且在微任务完成后才会执行渲染操作。
  • 事件循环会不断执行,任务队列依照“先进先出”原则。

应用

  1. 减轻大任务处理压力 假设当前我们有一百万条数据需要渲染。如果直接渲染,页面耗时会过长。可以通过 setTimeout(fn, 0) 函数将渲染操作分批加入宏任务, 每次渲染 100 条数据。
    let docBody = document.getElementById("body");
    function loop(nums) {
      if (nums <= 0) return;
      setTimeout(() => {
        for (let i = 0; i < 100; i++) {
          let div = document.createElement("div");
          div.innerHTML = i;
          docBody.appendChild(div);
        }
        loop(nums - 100);
      }, 0);
    }
    loop(100000);

练习

new Promise(resolve => {
    console.log(1);
    resolve();
}).then(() => {
    console.log(2);
})
console.log(3);

输出结果:1 3 2

Promise 本身是同步代码,then() 回调才属于微任务。

console.log(1);
async function async1() {
    await async2(); // 立即执行
    console.log(2); // 被加入微队列
    await async3(); // 被加入微队列
    console.log(3); // 被加入微队列
}
async function async2() {
    console.log(4);
}
async function async3() {
    console.log(5);
}
async1();
console.log(6);

输出结果:1 4 6 2 5 3

console.log(1);

setTimeout(() => console.log(2));

Promise.resolve().then(() => console.log(3));

Promise.resolve().then(() => setTimeout(() => console.log(4)));

Promise.resolve().then(() => console.log(5));

setTimeout(() => console.log(6));

console.log(7);

输出结果:1 7 3 5 2 6 4

console.log(1);
setTimeout(() => {
    console.log(2);
    Promise.resolve().then(() => console.log(3));
}, 0)
new Promise(resolve => {
    console.log(4);
    resolve();
}).then(() => console.log(5));
console.log(6);

输出结果 1 4 6 5 2 3

console.log(1);
async function async1() {
    await async2();
    console.log(2);
    await async3();
    console.log(3);
}
async function async2() {
    console.log(4);
}
async function async3() {
    console.log(5);
}
async1();
setTimeout(() => console.log(6), 0);
new Promise(resolve => {
    console.log(7);
    resolve();
}).then(() => console.log(8)).then(() => console.log(9));

输出结果 1 4 7 2 5 8 3 9 6