各位前端小伙伴们,从今天起每天复习+收藏一篇关于JavaScript面试知识点!
一、事件循环(Event Loop)基本概念
JavaScript 是单线程语言,同一时间只能执行一个任务。为了协调异步操作(如定时器、网络请求、用户交互),JavaScript 引入了事件循环机制。
- 调用栈(Call Stack) :执行同步代码的地方。
- 任务队列(Task Queue) :存放待处理的宏任务。
- 微任务队列(Microtask Queue) :存放待处理的微任务。
事件循环的流程是:
- 从宏任务队列中取出一个宏任务执行。
- 执行过程中产生的微任务会被添加到微任务队列。
- 宏任务执行完毕后,清空整个微任务队列(依次执行所有微任务)。
- 可能进行 UI 渲染(浏览器环境)。
- 从宏任务队列中取下一个宏任务,重复以上步骤。
一个形象的比喻
JavaScript 就像只有一个窗口的银行柜台,一次只能服务一个客户(执行一个任务)。
如果这个客户在办理业务时提出了一个“稍后处理”的需求(比如异步任务),柜员就会给他一个号,让他先去旁边等着(进入任务队列)。
这些等待的客户分成两种:
- VIP 客户(微任务) :比如预约过的、急需处理的,优先级高。
- 普通客户(宏任务) :比如普通排队的人。
柜员处理完当前客户后,会先叫号所有 VIP 客户(清空微任务队列),把他们的业务全部办完,然后再叫下一个普通客户(执行下一个宏任务)。
就这样循环往复,这就是事件循环。
它解决的核心问题:
- 避免主线程阻塞:JavaScript 是单线程的,如果每次遇到耗时操作(如网络请求、文件读写)都要原地等待,整个页面就会卡死,用户无法进行任何操作。
- 实现异步并发:通过事件循环,JavaScript 可以将异步任务交给浏览器或 Node.js 的其他线程处理,自己继续执行后续代码。当异步任务完成,对应的回调会被插入任务队列,等待主线程空闲时再执行。这样既利用了底层多线程能力,又保证了代码执行的顺序可控。
简单说,事件循环让 JavaScript 既能“一心一意”地执行代码,又能“眼观六路”地响应各种事件,是异步编程的基石。
二、宏任务(MacroTask)与微任务(MicroTask)
| 类型 | 常见API | 执行时机 |
|---|---|---|
| 宏任务 | setTimeout、setInterval、setImmediate(Node)、I/O、UI 渲染、MessageChannel | 每轮事件循环执行一个 |
| 微任务 | Promise.then/catch/finally、MutationObserver、queueMicrotask、process.nextTick(Node) | 当前宏任务结束后、下一宏任务开始前,清空全部 |
注意:
async/await本质也是基于 Promise 的微任务。
三、执行顺序示例
看一个经典代码,感受执行顺序:
javascript
console.log('1'); // 同步
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
console.log('4'); // 同步
// 输出顺序:1, 4, 3, 2
解析:
- 先执行所有同步代码:输出
1、4。 - 遇到
setTimeout,将其回调放入宏任务队列。 - 遇到
Promise.then,将其回调放入微任务队列。 - 同步代码执行完毕,当前宏任务结束,开始清空微任务队列:输出
3。 - 微任务队列清空,可能 UI 渲染,然后取出下一个宏任务(
setTimeout回调):输出2。
四、async/await 在事件循环中的行为
async/await 是 Promise 的语法糖,其行为与 Promise 完全一致。
1. async 函数
async函数总是返回一个 Promise 对象。- 函数体内部的返回值会被
Promise.resolve()包装。
2. await 表达式
await会暂停当前async函数的执行,等待后面的 Promise 完成。- 当
await后面的 Promise 完成(fulfilled 或 rejected)后,会将后续代码作为一个微任务加入微任务队列。 - 注意:
await后面的代码相当于.then()中的回调。
示例一:基础 await
javascript
async function foo() {
console.log('2');
await null; // 相当于 await Promise.resolve(null)
console.log('4');
}
console.log('1');
foo();
console.log('3');
// 输出顺序:1, 2, 3, 4
解析:
- 同步输出
1。 - 调用
foo,同步输出2(await之前的部分是同步执行的)。 - 遇到
await null,立即将后续代码(console.log('4'))作为微任务加入队列,并让出线程,返回。 - 继续执行同步代码,输出
3。 - 同步代码执行完毕,当前宏任务结束,清空微任务队列,输出
4。
示例二:await 后面是一个真正的 Promise
javascript
async function bar() {
console.log('x');
await new Promise(resolve => {
setTimeout(() => {
console.log('y');
resolve();
}, 0);
});
console.log('z');
}
console.log('a');
bar();
console.log('b');
// 输出顺序:a, x, b, y, z
解析:
- 同步:
a,进入bar同步输出x。 await后是一个 Promise,该 Promise 内部有一个setTimeout(宏任务)。await会使bar函数暂停,并将后续代码(console.log('z'))暂存。- 继续同步输出
b。 - 同步结束,宏任务队列开始处理:执行
setTimeout回调,输出y,并调用resolve()。 - 此时 Promise 状态变为 resolved,触发
bar中await后面的微任务,输出z。 - 注意:
z是在y之后输出的,因为setTimeout回调是宏任务,执行完后清空微任务队列时才会执行z。
五、综合复杂示例
javascript
async function test() {
console.log('1');
await new Promise(resolve => {
console.log('2');
resolve();
});
console.log('3');
}
setTimeout(() => console.log('4'), 0);
new Promise(resolve => {
console.log('5');
resolve();
}).then(() => console.log('6'));
test();
console.log('7');
// 输出顺序:1, 5, 2, 7, 6, 3, 4
逐步分析:
-
同步执行:
-
setTimeout回调加入宏任务队列。 -
执行 Promise 构造器(同步):输出
5,resolve()后then回调加入微任务队列。 -
调用
test():- 同步输出
1。 - 执行
new Promise构造器(同步):输出2,resolve()后await后续代码(console.log('3'))加入微任务队列。 test返回,继续。
- 同步输出
-
输出
7。
-
-
同步结束,当前宏任务执行完毕,开始清空微任务队列:
- 微任务1:
then回调输出6。 - 微任务2:
await后续输出3。
- 微任务1:
-
微任务清空,取出下一个宏任务:
setTimeout回调输出4。
最终顺序:1, 5, 2, 7, 6, 3, 4
六、注意事项与面试考点
- 微任务优先级高于宏任务:每次宏任务结束后都会清空所有微任务。
- async/await 是 Promise 的语法糖,其后续代码等同于
.then(),属于微任务。 - Promise 构造器内部的代码是同步执行的,只有
then/catch/finally是微任务。 await后面如果不是 Promise 对象,会被Promise.resolve()包装。- 事件循环在浏览器和 Node.js 中的差异(Node 中还有
process.nextTick、setImmediate等),但面试通常以浏览器环境为主。
七、总结
理解事件循环需要记住两个核心:
- 执行顺序:同步代码 → 微任务 → 宏任务(循环往复)。
- async/await 的本质:
await之前的代码同步执行,await之后的代码作为微任务等待。
掌握这些,再配合画图分析复杂的嵌套场景,就能从容应对面试中的事件循环题目。
如果这篇这篇文章对您有帮助?关注、点赞、收藏,三连支持一下。
有疑问或想法?评论区见。