# 为什么会有 event loop
JavaScript 中的任务分为同步和异步两种,它们的处理方式也各自不同。同步任务是直接放在主线程上排队依次执行的;异步任务会放在任务队列中,若有多个异步任务,则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到调用栈,然后主线程执行调用栈中的任务。
调用栈
调用栈是一个栈结构,函数调用会形成一个栈帧,帧中包含了当前执行函数的参数和局部变量等上下文信息,函数执行完后,它的执行上下文会从栈中弹出。
JavaScript 是单线程的,单线程是指 JS 引擎中解析和执行 JS 代码的线程只有一个(主线程),每次只能做一件事情。然而 ajax 请求中,主线程在等待响应的过程中会去做其他事情,浏览器先在事件表中注册 ajax 的回调函数,响应回来后回调函数被添加到任务队列中等待执行,不会造成线程阻塞,所以说 JS 处理 ajax 请求的方式是异步的。
综上所述,检查调用栈是否为空以及将某个任务添加到调用栈中的这个过程就是 event loop,这就是 JavaScript 实现异步的核心。
# 浏览器中的 event loop
可以通过 chrome://tracing/
来观察浏览器中的 event loop。
# event loop 示意图
上面这张图展示的就是浏览器中的 event loop,它包含两部分:
左边部分是堆空间和栈空间。JS 代码运行时,基础变量会存在栈空间里面,而复杂的变量,比如对象、函数等会存在堆空间里。同时,栈空间里也会保存一些对堆空间里的变量的引用。除此之外,栈里还会存函数的执行逻辑,比如 A 函数调用 B 函数,那么栈空间里就会既有 A 又有 B,并且是先执行 A 再执行 B,执行完成后,先释放 B,再释放 A。
右上角部分是异步队列。如果在执行过程中有一些比较耗时的异步事件,比如点击事件、ajax 请求、定时器等,就先会把它们放到这个异步队列中。当栈里面的主线程的同步任务执行完之后,就会来异步队列中取异步任务执行。由于队列是先进先出的结构,所以先进来的异步任务会先执行。如果此时异步队列中没有异步任务,那么浏览器也会处于 event loop 状态,实时监听异步队列中有没有异步任务,有的话就取出来执行。
# 浏览器中的宏任务和微任务
宏任务(macro-task):setTimeout、setInterval、script(整体代码)、I/O 操作、UI 渲染等。
微任务(micro-task):new Promise().then(回调)、MutationObserver (opens new window) 等。
任务执行顺序如下:
检查 macrotask 队列是否为空,非空则逐个执行 macrotask 中的任务,直到 macrotask 为空。
然后继续检查 microtask 队列是否为空,非空则逐个取出 microtask 中的任务执行,直到 microtask 为空。
最后执行视图更新。
宏微任务交替执行
当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。调用栈为空后,再次读取微任务队列里的任务,依次类推。
# event loop 练习题
🔒 1. 宏任务和微任务的执行顺序
setTimeout(() => {
console.log("timeout");
}, 0);
const promise = new Promise((resolve) => {
console.log("promise init");
resolve(1);
console.log("promise end");
});
promise.then((res) => {
console.log("promise result:", res);
});
// 执行结果:promise init、promise end、promise result:1、timeout
2
3
4
5
6
7
8
9
10
11
12
这段代码要注意的是 promise 的 resolve 函数中的代码是同步的,所以里面的两个 console.log 会先执行。然后 then 方法触发微任务,最后再执行宏任务。
同步任务:promise init、promise end,微任务:promise result:1、宏任务:timeouot。
🔒 2. 宏任务微任务交错执行
setTimeout(() => {
console.log("timeout1");
Promise.resolve().then(() => {
console.log("promise1");
});
}, 0);
Promise.resolve().then(() => {
console.log("promise2");
setTimeout(() => {
console.log("timeout2");
}, 0);
});
// 执行结果:promise2、timeout1、promise1、timeout2
2
3
4
5
6
7
8
9
10
11
12
13
首先,第一个宏任务 setTimeout 会先被放到宏任务队列中,但是暂时不会执行。接着先执行微任务 promise,打印 promise2,执行的同时把里面的 setTimeout 也放到了宏任务队列中,暂时也不会执行,此时这个 promise 就执行完成了。然后就会去宏任务队列中取出第一个 setTimeout 出来执行,打印 timeout1,此时又插入了一个微任务 promise,所以就会先执行这个微任务,打印 promise1,最后才去宏任务队列中取出第二个 setTimeout 执行,打印 timeout2。
🔒 3. async/await 拆解
🔔 如果 await 后是一个简单类型,则进行 Promise 包裹。
🔔 如果 await 后是一个 thenable 对象,则不用进行 Promise 包裹(chorme 的优化)。
async function fn() {
return await 1234;
}
fn().then((res) => console.log(res));
// 执行结果:1234
2
3
4
5
上面的代码打印出 1234,说明 async 返回的是一个 promise 对象,上面的代码等价于下面这样:
async function fn() {
return Promise.resolve(1234);
}
fn().then((res) => console.log(res));
// 执行结果:1234
2
3
4
5
await thenable
async function fn() {
return await {
then(resolve) {
resolve(1234);
}
};
}
fn().then((res) => console.log(res));
// 执行结果:1234
2
3
4
5
6
7
8
9
await 不加也是可以的,执行结果是一样的。
async function fn() {
return {
then(resolve) {
resolve(1234);
}
};
}
fn().then((res) => console.log(res));
// 执行结果:1234
2
3
4
5
6
7
8
9
如果返回的是一个包含 then 方法的对象(即 thenable 对象),那么根据 promise A+ 规范会将它当成一个 promise 对象去处理,调用里面的 then 方法输出 resolve 的内容。
如果 resolve 返回值还是一个包含 then 方法的对象,那么会继续递归调用 promise.then,直到 resolve 返回值是一个基础类型。
async function fn() {
return {
then(resolve) {
resolve({
then(r) {
r(1);
}
});
}
};
}
fn().then((res) => console.log(res));
// 执行结果:1
2
3
4
5
6
7
8
9
10
11
12
13
🔒 4. 使用 async await 顺序判断(学会将 async/await 转换成 Promise 来解决)
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
console.log("script");
// 执行结果:async1 start、async2、script、async1 end
2
3
4
5
6
7
8
9
10
11
async1 中的第一个 console 和 async2 中的 console 都是同步任务,因为 async 返回的是一个 Promise 对象,上面说过,promise 的 resolve 中的代码都是同步任务。所以这两者会先执行,接着执行 console.log('script') 这个同步任务,最后才执行 async1 中的第二个 console(微任务)。
可以将 async2 转换成一个 Promise 对象,就容易理解了:
async function async1() {
console.log("async1 start");
new Promise((resolve) => {
console.log("async2");
resolve();
}).then((res) => {
console.log("async1 end");
});
}
async1();
console.log("script");
// 执行结果:async1 start、async2、script、async1 end
2
3
4
5
6
7
8
9
10
11
12
🔒 5. 如果 promise 没有 resolve 或 reject
async function async1() {
console.log("async1 start");
await new Promise((resolve) => {
console.log("promise1");
});
console.log("async1 success");
return "async1 end";
}
console.log("script start");
async1().then((res) => console.log(res));
console.log("script end");
// 执行结果:script start、async1 start、promise1、script end
2
3
4
5
6
7
8
9
10
11
12
🔔 由于 promise 对象没有执行 resolve 或者 reject 方法,导致它的状态没有变更,这个 promise 永远没有完成,所以 await 下边的代码就永远不会执行。也就是说,await 下面的代码等到它返回的 peomise 对象的状态变更了才会执行,否则永远不会执行。
🔒 6. 某大厂的真实面试题
通过上面的学习,这道题我自己答对了哈哈哈~ 😄
async function async1() {
console.log("async1 start"); // 2
await async2();
console.log("async1 end"); // 6
}
async function async2() {
console.log("async2"); // 3
}
console.log("script start"); // 1
setTimeout(function() {
console.log("setTimeout"); // 10
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1"); // 4
resolve();
})
.then(function() {
console.log("promise2"); // 7
})
.then(function() {
console.log("promise3"); // 8
})
.then(function() {
console.log("promise4"); // 9
});
console.log("script end"); // 5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
执行结果如下:
🔒 7. resolve 处理 thenable,也会包裹一层 promise
这道题是第 6 题的变种。
这道题我也答对了哈哈哈~ 😄
async function async1() {
console.log("async1 start"); // 1
return new Promise((resolve) => {
resolve(async2());
}).then(() => {
console.log("async1 end"); // 4
});
}
function async2() {
console.log("async2"); // 2
}
setTimeout(function() {
console.log("setTimeout"); // 8
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1"); // 3
resolve();
})
.then(function() {
console.log("promise2"); // 5
})
.then(function() {
console.log("promise3"); // 6
})
.then(function() {
console.log("promise4"); // 7
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
接下来对这段代码改动下:
在 async2 前面加上 async。
async function async1() {
console.log("async1 start"); // 1
return new Promise((resolve) => {
resolve(async2());
}).then(() => {
console.log("async1 end"); // 6
});
}
async function async2() {
console.log("async2"); // 2
}
setTimeout(function() {
console.log("setTimeout"); // 8
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1"); // 3
resolve();
})
.then(function() {
console.log("promise2"); // 4
})
.then(function() {
console.log("promise3"); // 5
})
.then(function() {
console.log("promise4"); // 7
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
可以看到,此时 ‘async1 end’ 出现的位置变成了在 ‘peomise3’ 和 ‘promise4’ 之间了。
🔔 这是因为 async2 前面加了 async 之后,会返回一个 promise,也就是说此时它本身是一个 promise,在 async1 内部 resolve 处理 thenable 时又会再包裹一层 promise,所以它下面的代码就会比之前慢了两个位置输出。
再改一下:
async2 内部返回一个 thanable 对象
async function async1() {
console.log("async1 start"); // 1
return new Promise((resolve) => {
resolve(async2());
}).then(() => {
console.log("async1 end"); // 5
});
}
function async2() {
console.log("async2"); // 2
return {
then(r) {
r();
}
};
}
setTimeout(function() {
console.log("setTimeout"); // 8
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1"); // 3
resolve();
})
.then(function() {
console.log("promise2"); // 4
})
.then(function() {
console.log("promise3"); // 6
})
.then(function() {
console.log("promise4"); // 7
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
此时 ‘async1 end’ 出现的位置变成在 ‘peomise2’ 和 ‘promise3’ 之间了。
这是因为 async2 里面返回了一个 thenable 对象,此时虽然它本身不是一个 promise 了,但是在 async1 内部 resolve 处理 thenable 时还是会包裹一层 promise,所以它下面的代码就会比之前慢了一个位置输出。
如果 async2 里面返回的是一个基本类型,比如 return 1,那么输出结果就会跟第一次的一样。
# 面试真题
const first = () =>
new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
console.log(p);
}, 0);
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
});
first().then((arg) => {
console.log(arg);
});
console.log(4);
// 输出为:3、7、4、1、2、undefined、5、Promise {<fulfilled>: 1}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Node.js 中的 event loop
Node.js 中的 event loop 和浏览器中的是完全不相同的东西。
Node.js 采用 V8
作为 JS 的解析引擎,而 I/O 处理方面使用了自己设计的 libuv
。libuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统的一些底层特性,对外提供统一的 API,事件循环机制也是它里面的实现。
# 整体流程图
根据上图,Node.js 的运行机制如下:
V8 引擎解析 JavaScript 脚本。
解析后的代码,调用 Node API。
libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个 event loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。
V8 引擎再将结果返回给用户。
# 什么是 libuv
libuv (opens new window) 是⼀个跨平台的异步 I/O 库,它被⽤于 Node.js 的底层实现,提供了⾮阻塞 I/O 操作的⽀持。
在 node.js 启动时,创建了一个类似
while(true)
的循环体,每次执行一次循环体称为一次tick
,类似于饭店的厨师。每个 tick 的过程就是查看是否有事件等待处理,如果有,则取出事件及其相关的回调函数并执行,然后执行下一次 tick。
它的执行逻辑是,先询问事件观察者当前是否有任务需要执行?观察者回答 “有”,于是取出 A 执行,A 是否有回调函数?如果有(如果没有则继续询问当前是否有任务需要执行),则取出回调函数并执行(注意:回调函数的执行基本都是异步的,可能不止一个回调),执行完回调后通过某种方式通知调用者,我执行完了,并把执行结果给你,主函数不需要不断询问回调函数执行结果,回调函数会以通知的方式告知调用者我执行完了,而这个过程主线程并不需要等待回调函数执行完成,它会继续向前执行,直到观察者回答没有了,线程结束。
事件循环是一个典型的生产者、消费者模型。异步 IO、网络请求等则是事件的生产者,源源不断为 Node 提供不同类型事件,这些事件被传到观察者那里,事件则从观察者那里取出事件并处理。
# libuv 的 6 个阶段
Node.js 启动时会初始化由 libuv 提供的事件循环,每次的事件循环都包含 6 个阶段,这 6 个阶段会在每次事件循环中按顺序反复执行。
每当进入某一个阶段的时候,都会从对应的回调队列中取出函数执行。当队列为空或者执行的回调函数数量达到系统设定的阈值时,就会进入下一阶段。
事件循环开始前会获取一下系统时间 update_time,以保证之后的 timer 有个计时的标,避免过多的系统调用影响性能。
1. timer 阶段
处理那些已经到期的定时器回调。
在此阶段, libuv 会检查所有定时器,看它们是否到期,然后执⾏相应的回调函数。
2. I/O callback 阶段
处理⼤多数类型的回调,例如 TCP、UDP、⽂件 I/O 等。
在这个阶段, libuv 处理⼏乎所有事件源的回调。
3. idle,prepare 阶段
仅 node 内部使用。
4. poll 阶段
执⾏ I/O 轮询操作,检查哪些 I/O 操作已经准备好被执⾏。
此阶段包括两个部分:轮询新事件(poll for new events)和执⾏已经准备好的 I/O 操作。
如果没有其他回调要处理,libuv 会在此阶段等待直到最⼩的定时器到期。
5. check 阶段
执行 setImmediate() 的回调。
6. close callback 阶段
处理关闭请求和关闭事件的回调。例如,当⼀个 TCP socket 关闭时执⾏的回调。
# libuv 的观察者
libuv 库在其事件循环中管理着多种类型的观察者(observers)。这些观察者负责监听各种类型的事件,并在特定的事件循环阶段被激活。
1. 定时器观察者(Timer)
uv_timer_t。
负责处理定时器回调。
当定时器到期时,定时器观察者会被激活。
2. I/O 观察者(I/O)
uv_poll_t、uv_fs_event_t、uv_signal_t。
监听⽂件描述符上的 I/O 操作(如读写)的完成。
当相关的 I/O 操作准备就绪时,I/O 观察者会被激活。
3. 空闲观察者(Idle)
uv_idle_t。
在事件循环空闲时执⾏操作。
主要⽤于在系统空闲时进⾏低优先级的背景处理。
4. 准备观察者(Prepare)
uv_prepare_t。
在每次事件循环的 I/O 轮询阶段之前被激活。
可以⽤来准备⼀些必要的操作,或者预先执⾏⼀些任务。
5. 检查观察者(Check)
uv_check_t。
在每次事件循环的 I/O 轮询阶段之后被激活。
通常与 setImmediate() 功能⼀起使⽤,在 Node.js 中⽤来执⾏那些被 setImmediate() 延迟的回调。
6. 关闭观察者(Close)
uv_close_t。
用于处理资源关闭事件。
当资源需要关闭时,执行回调函数。
7. 异步观察者(Async)
uv_async_t。
用于在不同线程中唤醒事件循环并执行回调函数。
在多线程环境中使用,通过异步观察者向事件循环中插入异步事件。
8. 线程观察者(Thread)
uv_thread_t。
用于监视和管理其他线程。
允许主线程监控其他线程的状态,并在需要的时候等待其他线程的完成。
9. 信号观察者(Signal)
uv_signal_t。
监听系统信号,如 SIGINT、SIGTERM。
当这些信号发⽣时,信号观察者会被激活。
# Node.js 中的宏任务和微任务
宏任务(macro-task):setTimeout、setInterval、setImmediate、IO。
微任务(micro-task):Promise(async)、process.nextTick。
# Node.js 中的 event loop 过程
执行全局 script 的同步代码。
执行 microtask 微任务,先执行所有 Next Tick Queue 中的所有任务,再执行 Other Micro Queue 中的所有任务。
开始执行 macrotask 宏任务,共 6 个阶段,从第 1 个阶段开始执行相应每一个阶段 macrotask 中的所有任务(注意,这里的所有是指每个阶段宏任务队列的所有任务,在浏览器的 event loop 中只是取宏队列中的第一个任务出来执行)。每一个阶段的 macrotask 任务执行完毕后,开始执行微任务,也就是步骤 2。总的来说是如下过程:
Timers Queue -> 步骤 2 -> I/O Queue -> 步骤 2 -> Check Queue -> 步骤 2 -> Close Callback Queue -> 步骤 2 -> Timers Queue......
以上就是 Node.js 中的 event loop。
# 不同 Node.js 版本中的 event loop
1. node11 版本之前
一旦执行一个阶段,会先将这个阶段里的所有任务执行完成之后,才会执行该阶段剩下的微任务。
2. node11 版本之后
一旦执行一个阶段里的一个宏任务,就立刻执行对应的微任务队列。
# Node.js 中的任务比较练习题
🔒 1. 比较 setImmediate 和 setTimeout 的执行顺序
setTimeout((_) => console.log("setTimeout"));
setImmediate((_) => console.log("setImmediate"));
2
执行结果如下:
可以看到,一开始执行的时候都是先输出 setTimeout,但是后面突然就先输出 setImmediate 了。所以说明这两个的执行顺序是不固定的,跟机器的性能有关。
🔔 setTimeout 是在 timers 阶段执行的,setImmediate 是在 check 阶段执行的。
如果 setTimeout 先加入到 timers 中,那么在执行完 poll 阶段后,就会先执行 timers ,再执行 check。否则的话就会先执行 check 再执行 timers。
🔒 2. 如果两者都在 poll 阶段注册,那么顺序就能确定
const fs = require("fs");
fs.readFile("./test.html", () => {
setTimeout((_) => console.log("setTimeout"));
setImmediate((_) => console.log("setImmediate"));
});
2
3
4
5
执行结果如下:
可以看到,不管执行几次,输出顺序就是固定的了,总是先输出 setImmediate,再输出 setTimeout。
这是因为两者都是在 poll 阶段注册的,那么 timers 阶段肯定来不及加入 setTimeout,所以 poll 阶段执行完毕后,就执行 check 阶段。
🔒 3. 理解 process.nextTick
每一个阶段执行完成之后,在当前阶段尾部触发 nextTick。
案例:常见的 node.js 回调函数第一个参数,都是抛出的错误。
function apiCall(arg, callback) {
if (typeof arg !== "string") {
return process.nextTick(
callback,
new TypeError("argument should be string")
);
}
}
2
3
4
5
6
7
8
🔔 node.js 所有 api 的回调函数的第一个参数都是错误对象 err。
🔒 4. 比较 process.nextTick 和 setImmediate
🔔 process.nextTick() 在同一个阶段尾部立即执行,也就是每个阶段的开始之前都会执行。比如从 poll 阶段到 check 阶段,肯定会先执行 process.nextTick()。
🔔 setImmediate() 在事件循环的 check 阶段触发。
setImmediate(() => {
console.log("setImmediate");
});
process.nextTick(() => {
console.log("nextTick");
});
2
3
4
5
6
执行结果如下:
可以看到,nextTick 每次都比 setImmediate 先输出。
# 关于不同版本变化的几个 demo
🔒 timers 阶段的执行时机变化
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
}, 0);
setTimeout(() => {
console.log("timer2");
Promise.resolve().then(function() {
console.log("promise2");
});
}, 0);
2
3
4
5
6
7
8
9
10
11
12
13
执行结果:
node11 之前:timer1、timer2、promise1、promise2;
node11 之后:timer1、promise1、timer2、promise2。
🔒 check 阶段的执行时机变化
setImmediate(() => console.log("immediate1"));
setImmediate(() => {
console.log("immediate2");
Promise.resolve().then(() => console.log("promise resolve"));
});
setImmediate(() => console.log("immediate3"));
setImmediate(() => console.log("immediate4"));
2
3
4
5
6
7
执行结果:
node11 之前:immediate1、immediate2、immediate3、immediate4、promise resolve;
node11 之后:immediate1、immediate2、promise resolve、immediate3、immediate4。
🔒 nextTick 队列的执行时机变化
setImmediate(() => console.log("immediate1"));
setImmediate(() => {
console.log("immediate2");
process.nextTick(() => console.log("nextTick"));
});
setImmediate(() => console.log("immediate3"));
setImmediate(() => console.log("immediate4"));
2
3
4
5
6
7
执行结果:
node11 之前:immediate1、immediate2、immediate3、immediate4、nextTick;
node11 之后:immediate1、immediate2、nextTick、immediate3、immediate4。
🔔 从这里也可以看出,随着 node 版本的升高,它的 event loop 的表现跟浏览器的保持一致。
# 浏览器中的 event loop 和 Node.js 中的 event loop 的区别
# 浏览器的事件循环
1. 宏任务(Macro Tasks)和微任务(Micro Tasks)
浏览器的事件循环区分宏任务(如 setTimeout、setInterval)和微任务(如 Promise.then、MutationObserver)。
每次事件循环,浏览器会先执⾏⼀个宏任务,然后执⾏所有的微任务。
2. 渲染操作
浏览器的事件循环与浏览器的渲染⾏为紧密相关。例如,requestAnimationFrame 是与浏览器的绘制(如重绘和回流)相对应的。
3. ⽤户交互
浏览器的事件循环还处理⽤户交互事件,如点击、滚动等。
# Node.js 的事件循环
1. 由 libuv 库实现
Node.js 使⽤ libuv 库实现其事件循环。libuv 提供了跨平台的异步 I/O 能⼒。
2. 阶段性处理
Node.js 的事件循环包含⼏个明确的阶段,如定时器阶段、I/O 回调阶段、setImmediate 阶段等。
每个阶段都有特定的任务类型,事件循环会在这些阶段间切换。
3. 不直接处理渲染
与浏览器不同,Node.js 不处理任何渲染任务,因为它不是⼀个浏览器环境。
4. 处理后端服务
Node.js 主要⽤于服务器端应⽤,事件循环处理的是⽹络请求、⽂件 I/O 等后端服务相关的任务。
# 主要区别
1. 任务类型和处理⽅式
浏览器的重点是处理⽤户界⾯相关的任务,如事件处理、渲染等,⽽ Node.js 的重点是处理 I/O 任务。
2. 事件循环机制
浏览器的事件循环是 HTML 规范的⼀部分,⽽ Node.js 的事件循环是基于 libuv 实现的。
3. 微任务处理
在浏览器中,微任务在每个宏任务之后⽴即执⾏;在 Node.js 中,微任务的执⾏时机可能会根据事件循环的阶段⽽有所不同。
← 原型链总结 JavaScript 基础测试 →