事件循环整体解释
JS是一门单线程的非阻塞的脚本语言,这么实现主要为了避免,同时修改DOM信息已出现信息不同步问题。为了解决并发问题,引入了事件循环机制。
事件循环是一种在单线程环境下实现并发的方式,它的核心思想是将异步任务放入任务队列中,通过在主线程空闲时从队列中取出任务并执行。这个过程会不断地循环,从而实现了对异步事件的处理。
任务被分成两种:同步任务(synchronous)和异步任务(asynchronous):
- 同步任务:指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务:指一个任务不是连续完成的,先执行第一段,做好准备,再执行第二段(也叫回调),这个过程耗时长。这中间过程是由I/O去执行的,JS不需要参与,只需等待返回的结果再执行回调。
执行流程
- 所有同步任务都在主线程上执行,形成调用栈
- 主线程之外还存在任务队列。只要异步任务运行有了结果就会在任务队列中放置一个回调。
- 一旦执行栈中所有同步任务执行完毕(调用栈为空),系统就会读取任务队列中的回调放到主线程执行
- 主线程不断重复上面的一步。
任务队列内部,任务大致分两类:微任务和宏任务。任务队列也分为两种:任务队列(也叫宏任务队列,macrotask queue)和微任务队列(microtask queue)
事件循环介绍
JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其他语言中的模型截然不同,比如 C 和 Java。
栈(方法)
function foo(b) {
let a = 10;
return a + b + 11;
}
function bar(x) {
let y = 3;
return foo(x * y);
}
console.log(bar(7)); // 返回 42
当调用 bar 时,第一个帧被创建并压入栈中,帧中包含了 bar 的参数和局部变量。当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧)。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。
堆(对象)
对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。
队列
一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。
在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。
函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。
零延迟
零延迟并不意味着回调会立即执行。以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调用回调函数。
其等待的时间取决于队列里待处理的消息数量。在下面的例子中,"这是一条消息" 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间
详细说明
Event Loop(事件循环)是 JavaScript 的核心机制之一,它负责处理异步操作,如回调函数、网络请求、定时器等,确保 JavaScript 作为单线程语言能够高效处理并发任务。理解 Event Loop 对于优化前端应用的性能以及处理异步操作非常关键。
Event Loop 的工作原理
JavaScript 是单线程语言,这意味着它一次只能执行一个任务。如果遇到长时间执行的任务或阻塞操作,整个应用就会卡住。为了解决这个问题,JavaScript 引入了 Event Loop 来处理异步任务,使其不会阻塞主线程的执行。
Event Loop 的核心步骤
执行栈(Call Stack):
- 执行栈用于存储当前运行的函数。当函数被调用时,它被推入执行栈,函数执行完毕后,它会被从栈中移除。
- 同步代码直接在执行栈中执行。
任务队列(Task Queue):
- 异步操作(如定时器、网络请求、DOM 事件等)在完成后,会将回调函数放入任务队列。
- 当执行栈中的所有同步任务完成后,Event Loop 会从任务队列中取出任务并将其推入执行栈,随后执行这些异步任务。
微任务队列(Microtask Queue):
- 微任务通常包括
Promise
的回调和MutationObserver
。 - 在每次执行栈为空时,Event Loop 先处理微任务队列中的所有任务,然后再处理宏任务队列中的任务。
- 微任务通常包括
宏任务 vs 微任务
- 宏任务: 包括
setTimeout
、setInterval
、I/O
操作、事件监听等,属于普通任务队列。 - 微任务: 包括
Promise.then
回调、MutationObserver
,这些任务优先执行。
Event Loop 执行顺序示例
console.log("Start");
setTimeout(() => {
console.log("Macro Task - Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Micro Task - Promise");
});
console.log("End");
执行结果:
Start
End
Micro Task - Promise
Macro Task - Timeout
解释:
console.log("Start")
和console.log("End")
是同步任务,直接在执行栈中执行。setTimeout
是宏任务,它的回调会被放入宏任务队列,等到同步任务和微任务都执行完后再执行。Promise.resolve().then()
是微任务,它的回调被放入微任务队列,在当前宏任务执行完后立即执行。- 最后执行
setTimeout
的回调,这是宏任务。
Event Loop 的实际应用场景
1. 处理异步操作
- 网络请求: 如
fetch
、XHR
请求等,这些操作都是异步的。当数据返回时,它们的回调函数会被推入任务队列。 - 定时器:
setTimeout
、setInterval
等操作不会立即执行,而是会在指定时间后将回调放入任务队列等待执行。
示例:
console.log("Fetching data...");
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
console.log("Done!");
由于 fetch
是异步的,控制台会先输出 "Fetching data..." 和 "Done!",然后在数据返回时输出数据。
2. UI 渲染与用户交互
浏览器的渲染引擎与 JavaScript 引擎协同工作,渲染操作通常会在执行 JavaScript 任务和处理微任务之后进行。通过合理使用异步操作,避免阻塞主线程的渲染,可以提升页面性能。
示例:
- 在用户滚动时执行某些操作时,使用
debounce
减少事件处理频率,确保滚动时页面保持流畅。
3. 性能优化
- 防止阻塞主线程: 长时间的同步任务(如复杂计算)会阻塞主线程,导致页面无响应。通过将复杂任务放入异步操作中,使用
setTimeout
或Web Worker
处理,可以避免主线程被阻塞。
示例: 假设你有一个需要执行的繁重计算任务,可以通过 setTimeout
将任务分批执行,让主线程有时间处理其他任务。
function heavyTask() {
for (let i = 0; i < 1000000000; i++) {
// 计算任务
}
}
setTimeout(heavyTask, 0); // 将繁重任务放入任务队列
4. Promise 的使用
Promise 是基于微任务机制的异步操作方式,它确保了回调函数在本轮事件循环结束时立即执行。
示例:
console.log('Start');
Promise.resolve().then(() => {
console.log('Promise fulfilled');
});
console.log('End');
输出顺序为:
Start
End
Promise fulfilled
5. 异步队列与调度
在高并发场景下,Event Loop 可以通过控制任务队列的执行顺序,调度异步任务,从而提升性能。例如,在数据流处理或批量任务处理中,Event Loop 保证任务按顺序执行,不会发生阻塞。
6. 动画和交互的流畅性
在前端开发中,频繁的 DOM 操作会影响动画的流畅性。通过使用 requestAnimationFrame
,你可以确保动画在每一帧绘制时执行,不会造成卡顿。
示例:
function animate() {
// 更新 DOM 的动画状态
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
requestAnimationFrame
将任务放入任务队列,以确保在下一帧渲染前执行回调,优化动画效果。
总结
Event Loop 是 JavaScript 处理异步操作的关键机制,它通过协调执行栈和任务队列,使得单线程 JavaScript 也能高效地处理并发任务。在前端开发中,理解 Event Loop 可以帮助我们优化代码执行顺序,处理异步请求,防止主线程阻塞,以及提升动画和交互的流畅性。
Event Loop 的实际应用不仅体现在网络请求和定时器上,还涉及到复杂的性能优化场景。通过合理运用 Event Loop,可以有效提升前端应用的用户体验和响应速度。
常考题目
console.log("Begin");
setTimeout(() => {
console.log("setTimeout");
}, 0);
new Promise((res, rej) => {
console.log("Promise");
res();
}).then(()=> {
console.log('Then')
});
console.log("End")