事件循环 Event Loop
基本概念
事件循环(Event Loop)是 JavaScript 运行时环境中负责管理异步任务执行的一种机制。在浏览器中,事件循环是由浏览器的 JavaScript 引擎(如 V8 引擎)负责实现;在 Node.js 等环境中,也有自己的事件循环实现。简单来说,事件循环就是轮询任务队列,执行任务,休眠的无限循环。
事件循环的意义
原因很简单,JavaScript 是单线程语言,单线程意味着同一时间只能执行一个任务,为了能有效处理异步任务,而不会阻塞主线程,就需要并发处理,而事件循环就是并发的一种形式。这对于交互式的 Web 应用和处理网络请求等操作至关重要。
任务队列
任务队列是事件循环的核心组成部分,用于管理和调度异步任务的执行顺序。当任务到来时,JavaScript 引擎可能处于忙碌状态,那么任务会被排入队列里。
队列中的任务遵循 “先进先出” 的原则。当浏览器引擎执行完 script 后,它会处理 setTimeout 事件,然后处理 I/O 事件,以此类推。
Tips: 在执行任务时不会进行渲染操作,仅在任务完成后才会进行渲染;
任务花费的时间过长,浏览器将无法执行其他任务。会抛出一个 “页面未响应” 的警报。这种情况常发生在有大量复杂的计算或死循环的程序错误时。
因为队列 “先进先出” 的原则,所以引入微任务队列来执行高优先级的任务。通过微任务可以让开发者更精确地控制异步操作的执行顺序和时机。
在每个宏任务执行之后,引擎会执行微任务队列中的所有任务,然后再执行其他宏任务。
Example:
setTimeout(() => console.log(1));
Promise.resolve()
.then(() => console.log(2));
console.log(3);
// 输出结果:3 2 1
- 首先输出 3 因为是常规同步调用;
- 输出 2,then 回调函数会被放入微任务队列中,并在当前代码结束后执行;
- 输出 1,timeout 属于宏任务。
引入微任务之后,事件循环算法优化为:
- 从宏任务队列中出队并执行任务。
- 查看微任务队列查看是否有任务:
- 当微任务队列非空时,执行微任务。
- 当微任务队列为空时,返回宏任务队列。
- 如果有 dom 变更,则进行渲染。
- 如果宏任务队列为空,休眠等待。
- 转到步骤 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) 添加微任务。
- 微任务的优先级会高于宏任务,当前宏任务执行完毕后立即执行微任务,并且在微任务完成后才会执行渲染操作。
- 事件循环会不断执行,任务队列依照“先进先出”原则。
应用
- 减轻大任务处理压力
假设当前我们有一百万条数据需要渲染。如果直接渲染,页面耗时会过长。可以通过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