各位观众老爷,大家好!我是今天的讲师,咱们今天就来聊聊 JavaScript 里那让人既爱又恨的 Event Loop。别怕,咱们不搞那些晦涩难懂的概念,就用最接地气的方式,把这玩意儿给扒个精光!
开场白:Event Loop 是个啥?
想象一下,你是一个餐厅服务员,顾客(浏览器)不断给你提需求(JavaScript 代码),比如“点个菜(运行一个函数)”,“结账(处理一个事件)”。你不可能同时处理所有事情,对吧?所以你需要一个工作流程,一个“循环”来处理这些请求。这个“循环”就是 Event Loop。
简单来说,Event Loop 就是 JavaScript 用来处理异步操作的一套机制。它保证了 JavaScript 代码可以非阻塞地运行,让你的网页不会卡死。
Event Loop 的核心组件
要理解 Event Loop,我们需要先认识几个关键的家伙:
-
调用栈 (Call Stack): 这是 JavaScript 运行代码的地方。想象成一摞盘子,你只能从最上面取盘子(执行函数)。函数被调用时,会被压入栈中;函数执行完毕,就会从栈中弹出。JavaScript 是单线程的,这意味着同一时间只能执行栈顶的函数。
-
任务队列 (Task Queue): 也叫回调队列 (Callback Queue)。这是存放待执行任务的地方。当一个异步操作完成时(比如
setTimeout计时结束,或者 AJAX 请求返回数据),它的回调函数就会被放入任务队列。 -
Event Loop本尊: 它就是一个无限循环,不断地检查调用栈是否为空。如果调用栈为空,它就会从任务队列中取出一个任务,放入调用栈中执行。
macrotask 和 microtask:任务队列的分类
任务队列并不是一个简单的“先进先出”的队列,而是分成了两类:macrotask (宏任务) 和 microtask (微任务)。
-
macrotask: 也叫做task。包括:setTimeoutsetIntervalsetImmediate(Node.js 特有)- I/O 操作 (比如文件读取,网络请求)
- UI 渲染
-
microtask: 也叫做jobs。包括:Promise.then/.catch/.finallyqueueMicrotask()(新增的 API)MutationObserver
macrotask 和 microtask 的执行顺序
这是 Event Loop 最核心的部分,也是最容易让人困惑的地方。让我们用一个比喻来解释:
想象你的任务队列是一个自助餐厅。macrotask 是主菜,microtask 是甜点。
Event Loop 的工作流程是这样的:
- 执行一个
macrotask: 从macrotask队列中取出一个任务,放入调用栈执行。 - 清空
microtask队列: 执行完一个macrotask后,会立即检查microtask队列。如果队列中有任务,就全部执行,直到队列为空。 - 更新渲染: 浏览器可能会更新渲染界面。
- 重复以上步骤: 然后再次从
macrotask队列中取下一个任务,重复上述过程。
用代码说话:实战演练
光说不练假把式,让我们来看几个例子,加深理解:
例子 1:setTimeout 和 Promise 的较量
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
你觉得这段代码会输出什么?
答案是:
script start
script end
promise1
promise2
setTimeout
为什么?
console.log('script start')被执行,输出 "script start"。setTimeout被调用,它的回调函数被放入macrotask队列。Promise.resolve().then(...)创建了一个Promise,它的then回调函数被放入microtask队列。console.log('script end')被执行,输出 "script end"。- 此时,调用栈为空,
Event Loop开始工作。 - 首先,它会找到
macrotask队列中的setTimeout回调函数,放入调用栈执行。 - 但是在此之前,它会先清空
microtask队列。 promise1被输出。promise1的then执行完毕,将promise2放入microtask队列。promise2被输出。microtask队列为空。- 现在,
setTimeout的回调函数被执行,输出 "setTimeout"。
例子 2:多个 Promise 的嵌套
console.log('start');
Promise.resolve().then(() => {
console.log('promise1');
return Promise.resolve();
}).then(() => {
console.log('promise2');
});
Promise.resolve().then(() => {
console.log('promise3');
});
console.log('end');
输出结果:
start
end
promise1
promise3
promise2
分析:
start和end先被输出,这个没啥疑问。promise1和promise3的then回调都被放入microtask队列。- 注意,
promise1的then回调返回了一个新的Promise。 microtask队列清空,promise1和promise3依次执行。promise1的then回调返回的Promise的then回调(也就是输出promise2的那个回调)被放入microtask队列。- 再次清空
microtask队列,promise2被输出。
例子 3:queueMicrotask() 的使用
queueMicrotask() 是一个新增的 API,允许你手动将一个函数放入 microtask 队列。
console.log('start');
queueMicrotask(() => {
console.log('microtask1');
});
Promise.resolve().then(() => {
console.log('promise1');
});
console.log('end');
输出结果:
start
end
microtask1
promise1
分析:
queueMicrotask 和 Promise.then 都会将回调函数放入 microtask 队列,它们的执行顺序取决于它们被放入队列的顺序。
表格总结:macrotask vs microtask
| 特性 | macrotask |
microtask |
|---|---|---|
| 类型 | 宏任务 | 微任务 |
| 例子 | setTimeout, setInterval, I/O, UI 渲染 |
Promise.then, queueMicrotask, MutationObserver |
| 执行时机 | 每个 macrotask 执行完毕后,清空 microtask 队列 |
在当前 macrotask 执行完毕后立即执行 |
| 优先级 | 低 | 高 |
| 对 UI 的影响 | 可能导致页面卡顿 | 通常不会导致页面卡顿 |
注意事项:microtask 的饥饿问题
如果 microtask 队列中有大量的任务,并且不断地产生新的 microtask,那么 Event Loop 可能会一直执行 microtask,导致 macrotask 无法得到执行,造成页面卡顿,甚至崩溃。这就是所谓的 microtask 的饥饿问题。
如何避免 microtask 饥饿?
- 避免在
microtask中执行耗时操作:microtask应该尽可能快地执行完毕。 - 限制
microtask的数量: 不要一次性创建大量的microtask。 - 考虑使用
macrotask代替microtask: 如果任务不是必须在当前macrotask结束后立即执行,可以考虑使用setTimeout将其放入macrotask队列。
async/await 的幕后功臣
async/await 是 JavaScript 中处理异步操作的语法糖。它让异步代码看起来像同步代码一样,提高了代码的可读性。
async/await 的底层实现也是基于 Promise 和 Event Loop 的。
async function myFunc() {
console.log('before await');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('after await');
}
myFunc();
这段代码的执行流程是这样的:
myFunc()被调用,输出 "before await"。await new Promise(...)会暂停myFunc()的执行,并将Promise的then回调(也就是输出 "after await" 的那部分代码)放入microtask队列。setTimeout会将计时结束后的回调放入macrotask队列。- 当
setTimeout的回调执行时,Promiseresolve,then回调被放入microtask队列. Event Loop再次循环,microtask队列被清空,输出 "after await"。
总结:Event Loop 的精髓
Event Loop 是 JavaScript 的核心机制之一,理解它对于编写高性能、响应迅速的 Web 应用至关重要。
Event Loop是一个无限循环,用于处理异步操作。macrotask和microtask是任务队列的两种类型,它们的执行顺序有严格的规定。microtask的优先级高于macrotask,但过度使用microtask可能会导致饥饿问题。async/await是基于Promise和Event Loop的语法糖,简化了异步代码的编写。
结尾:练习题
为了巩固大家的理解,给大家留一道练习题:
console.log('1');
setTimeout(function() {
console.log('2');
new Promise(function(resolve) {
console.log('3');
resolve();
}).then(function() {
console.log('4')
})
});
new Promise(function(resolve) {
console.log('5');
resolve();
}).then(function() {
console.log('6')
});
console.log('7');
setTimeout(function() {
console.log('8');
new Promise(function(resolve) {
console.log('9');
resolve();
}).then(function() {
console.log('10')
})
});
请分析这段代码的输出结果,并在评论区留下你的答案!
今天的讲座就到这里,希望大家有所收获!下次再见!