各位观众老爷,大家好!我是今天的讲师,咱们今天就来聊聊 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
。包括:setTimeout
setInterval
setImmediate
(Node.js 特有)- I/O 操作 (比如文件读取,网络请求)
- UI 渲染
-
microtask
: 也叫做jobs
。包括:Promise.then
/.catch
/.finally
queueMicrotask()
(新增的 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
的回调执行时,Promise
resolve,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')
})
});
请分析这段代码的输出结果,并在评论区留下你的答案!
今天的讲座就到这里,希望大家有所收获!下次再见!