事件循环 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