异步与事件循环
一、异步与事件循环
1.1 JS 是单线程的
同一时刻只能做一件事。 和 Python 不一样——Python 可以通过 threading 开多线程,JS 不行。
为什么设计成单线程? JS 最初为浏览器设计,用来操作 DOM。如果两个线程同时改同一个 DOM 元素(一个改文字、一个删元素),就崩了。所以单线程是 JS 的宿命。
// 单线程的后果:死循环会冻住整个页面
while (true) {} // 卡死!所有按钮、动画全冻住
console.log("我永远执行不到");
1.2 同步 vs 异步
同步 = 排队买奶茶,前面的人不走,你就死等。
console.log("1"); // ① 执行
console.log("2"); // ② 等①完了才执行
console.log("3"); // ③ 等②完了才执行
// 输出:1 2 3 —— 一行一行,严格按顺序,谁也别想插队
异步 = 点完餐拿号,叫到号才取,中间不站着干等。
console.log("1"); // ① 同步,立刻执行
setTimeout(function() { // ② "登记:1 秒后叫我"
console.log("2"); // ④ 1 秒后执行
}, 1000);
console.log("3"); // ③ 不等②,立刻执行
// 输出:1 3 2 —— 异步的 2 排到了最后
为什么要异步? 如果 JS 只能同步,发网络请求时整个页面冻住 3 秒——按钮点不动、动画卡住、什么都干不了。异步让”慢操作”不堵路,好了再回来处理。
// ❌ 如果 JS 只能同步(假设写法,实际不行):
const data = fetchSync("/api"); // 卡住 3 秒等响应,页面冻死
// ✅ 实际:异步
fetch("/api").then(data => { // 发出去就不管了,继续往下跑
console.log(data); // 等数据到了再回来处理
});
console.log("请求已发出,继续干别的"); // 不卡,立刻输出
setTimeout(fn, 0) 为什么也排最后?
console.log("1");
setTimeout(function() { console.log("2"); }, 0); // 哪怕 0 毫秒
console.log("3");
// 输出:1 3 2
// 因为 setTimeout 的回调永远是异步的——哪怕设 0ms,也要等同步代码全部跑完才轮到它
1.3 宏任务 vs 微任务
JS 的异步任务分两个队列,微任务优先级更高。
| 宏任务(Macrotask) | 微任务(Microtask) |
|---|---|
setTimeout | Promise.then() 的回调 |
setInterval | async/await 的 await 后续代码 |
| I/O(文件、网络请求) | queueMicrotask(fn) |
执行顺序(背下来):
graph TD S[① 执行当前宏任务<br/>首次是整段同步代码<br/>脚本本身就是一个宏任务] --> E[② 当前宏任务执行完] E --> C[③ 清空所有微任务队列<br/>一个不留] C --> N[④ 取下一个宏任务<br/>回到①] N --> S
即:宏1(同步)→ 微全清 → 宏2 → 微全清 → 宏3 → …
console.log("1"); // 同步
setTimeout(function() {
console.log("2"); // 宏任务
}, 0);
Promise.resolve().then(function() {
console.log("3"); // 微任务
});
console.log("4"); // 同步
// 输出:1 4 3 2
// 拆解:
// 1、4 是同步 → 先输出
// 微任务 3 优先级高 → 接着输出
// 宏任务 2 → 最后输出
同步 + 两个异步队列:跟着 JS 引擎大脑逐行走一遍
console.log("sync1"); // 行 A
setTimeout(function() { // 行 B
console.log("timeout1"); // 行 C
Promise.resolve().then(function() { // 行 D
console.log("micro1"); // 行 E
});
}, 0);
Promise.resolve().then(function() { // 行 F
console.log("micro2"); // 行 G
}).then(function() { // 行 H
console.log("micro3"); // 行 I
});
console.log("sync2"); // 行 J
sequenceDiagram participant Engine as JS引擎 participant Screen as 屏幕 participant MacroQ as 宏任务队列 participant MicroQ as 微任务队列 Note over Engine,MicroQ: === 第1轮宏任务: 整段同步代码 === Engine->>Screen: 行A: console.logsync1 → sync1 Engine->>MacroQ: 行B: setTimeout → 登记回调 Engine->>MicroQ: 行F: Promise.resolve → micro2回调入队 Note over Engine: 行H: micro3先挂着等 Engine->>Screen: 行J: console.logsync2 → sync2 Note over Engine,MicroQ: 同步代码跑完!<br/>宏任务: [setTimeout]<br/>微任务: [micro2] Note over Engine,MicroQ: === 清空微任务队列 === Engine->>MicroQ: micro2出列 MicroQ->>Screen: 输出 micro2 MicroQ->>MicroQ: micro2完成→micro3入队 Engine->>MicroQ: micro3出列 MicroQ->>Screen: 输出 micro3 Note over MicroQ: 微任务队列空了! Note over Engine,MicroQ: === 取下一个宏任务 === Engine->>MacroQ: setTimeout出列 MacroQ->>Screen: 输出 timeout1 MacroQ->>MicroQ: Promise.resolve→micro1入队 Note over MacroQ: 当前宏任务结束 Engine->>MicroQ: micro1出列 MicroQ->>Screen: 输出 micro1 Note over Screen: 最终: sync1 sync2 micro2 micro3 timeout1 micro1
规律总结:
graph LR Syn[同步代码] --> Micro[微任务全清] Micro --> Macro[宏任务一个] Macro --> Micro2[微任务全清] Micro2 --> Macro2[宏任务一个] Macro2 --> More[...] Micro -.-> M1[micro2 micro3<br/>micro2产出的micro3<br/>在本轮一同清空] Macro2 -.-> M2[timeout1产出的micro1<br/>将在下一轮清空]
1.4 Promise —— 处理异步的标准化方式
先回答两个基本问题:
// ===== Promise 是对象吗?=====
// Promise 是内置类(构造函数),new Promise() 创建出的实例是对象
typeof Promise; // "function" —— Promise 本身是构造函数
const p = new Promise(() => {});
typeof p; // "object" —— new 出来的是对象
// 所以:p 是一个 Promise 对象/实例,上面有 .then、.catch、.finally
// ===== resolve 和 reject 是内置函数吗?=====
// 不是!它们不是全局函数,不能像 console.log 一样到处调
// 它们是 Promise 内部自动传给"执行器函数"的两个参数:
new Promise(function(resolve, reject) {
// ^^^^^^^ ^^^^^^
// 这两个参数是 Promise 引擎内部创建好,自动传给你的
// 你的任务只是:在合适的时机调用 resolve() 或 reject()
});
// ❌ 在外面写 resolve("xx") → 报错!resolve 不是全局函数
// ✅ 只在 new Promise(fn) 的 fn 函数体内使用
| 问题 | 答案 |
|---|---|
Promise 是什么? | 内置构造函数,和 Array、Date 同一级别 |
new Promise() 产出什么? | 一个 Promise 实例对象,上面有 .then/.catch/.finally |
resolve / reject 是谁? | Promise 给执行器函数的参数,不是全局函数 |
Promise.resolve() 是什么? | Promise 上的静态方法,= new Promise(resolve => resolve("值")) 的简写 |
// ===== 创建 Promise =====
// new Promise(执行器函数) —— 执行器函数是同步执行的!
const p = new Promise(function(resolve, reject) {
// ↑ 这个函数体是同步的,new 的瞬间就执行
const ok = true;
if (ok) {
resolve("成功了"); // 调 resolve → Promise 状态变 fulfilled
} else {
reject("失败了"); // 调 reject → Promise 状态变 rejected
}
});
// 执行到这里时,上面的函数体已经跑完了
// ===== 三种状态 =====
// pending(进行中)→ resolve(value) → fulfilled(已完成)
// pending(进行中)→ reject(reason) → rejected(已拒绝)
// 状态一旦确定就不可逆,再调 resolve/reject 无效
// ===== 消费 Promise =====
p.then(function(value) {
console.log("成功:", value); // resolve 的值在这里拿到
}).catch(function(reason) {
console.log("失败:", reason); // reject 的值在这里拿到
}).finally(function() {
console.log("无论成败都执行");
});
// ===== 链式调用 =====
new Promise(resolve => resolve(1))
.then(v => v + 1) // 返回普通值 2 → 下一个 then 拿到 2
.then(v => v * 10) // 返回 20 → 下一个 then 拿到 20
.then(v => console.log(v)); // 20
// then 每次返回新 Promise,所以可以一直 .then().then().then()
1.5 ⚠️ 黄金陷阱:new Promise(fn) 的 fn 是同步的
console.log("A");
new Promise(function(resolve) {
console.log("B"); // ← 这个输出是同步的!
resolve();
});
console.log("C");
// 输出:A B C
// B 的执行器是同步代码,跟着 new Promise 一起立刻执行
// 只有 .then() 的回调才是异步的(微任务)
1.6 async/await —— Promise 的语法糖
“语法糖”什么意思? 换个更甜的写法,底层东西完全没变。就像”拿铁”和”浓缩咖啡+牛奶”——叫法不同,喝到嘴里是同一种东西。
// ===== "咖啡+牛奶"写法:Promise.then =====
function fetchData() {
return fetch("/api/user").then(data => data);
}
// ===== "拿铁"写法:async/await =====
async function fetchData() {
const data = await fetch("/api/user"); // await 等 Promise 完成
return data; // async 函数永远返回 Promise
}
// ↑ 两段代码跑起来效果一模一样,async/await 只是让你读起来更像"同步代码"
loadData() 逐行执行流程分析:
async function loadData() {
try { // ① 进入 try 块
const data = await fetchData(); // ② 调用 fetchData(),返回 Promise
// ──────── 同步部分结束,下面变微任务 ────────
// ③ await 等待 fetchData() 完成
// ④ 成功 → data = 返回值,继续往下
console.log(data); // ⑤ 输出拿到的数据
} catch (err) { // ⑥ 如果②~⑤任一步失败,跳到这
console.log("出错了:", err); // ⑦ 输出错误
}
}
loadData(); // async 函数调用,返回一个 Promise
sequenceDiagram participant Main as 主线程 participant LF as loadData participant FD as fetchData participant Micro as 微任务队列 Note over Main,Micro: 时刻1 Main->>LF: loadData被调用 LF->>LF: 进入try → 调用fetchData LF->>FD: 遇到await → 暂停! FD->>Micro: await后面代码塞进微任务队列 Note over LF: loadData暂时下班<br/>主线程继续跑同步代码 Note over Main,Micro: 时刻2 FD->>FD: fetchData完成 FD->>Micro: data有了值 → 微任务出列 Micro->>LF: data=返回值 LF->>LF: console.logdata → 输出 Note over Main,Micro: 时刻3 LF->>LF: try块结束 跳过catch LF->>LF: loadData返回的Promise变成fulfilled
sequenceDiagram participant Main as 主线程 participant LF as loadData participant FD as fetchData participant Catch as catch Note over Main,Catch: 时刻1: 同上 Note over Main,Catch: 时刻2 FD->>FD: fetchData reject了 FD->>LF: 错误抛出 LF->>Catch: try块中断 → 跳到catcherr Catch->>Catch: err=错误原因<br/>输出"出错了: ..." Note over Catch: 程序不会崩溃<br/>错误被catch兜住了
await 怎么读:
-
await x= “我要等 x 这个 Promise 完成,拿到它肚子里面的值” -
await后面的所有代码 =.then(() => { ... })里面的代码 = 微任务
// 等价转换——上面 loadData 等于下面这个 Promise 链:
function loadData() {
return fetchData()
.then(data => {
console.log(data); // try 块里的逻辑
})
.catch(err => {
console.log("出错了:", err); // catch 块
});
}
// async/await 版:读起来像同步代码
// Promise 链版:嵌套更多但直接暴露了"异步"本质
// 两者完全等价
“返回 Promise”到底什么意思?
// ===== async 函数的 return 自动包一层 Promise =====
async function getData() {
return "饭"; // 你 return 的是普通值
}
// ↑ JS 引擎悄悄把它变成:
// return Promise.resolve("饭");
const result = getData(); // result 不是 "饭",是 Promise 对象
console.log(result); // Promise { "饭" } —— 像外卖订单,不是饭本身
// ===== 要取里面的值,必须 .then() 或 await =====
getData().then(food => {
console.log(food); // "饭" —— 拆开订单
});
const food = await getData(); // "饭" —— await 就是拆订单
const food = await getData() 拆解:
// 分两步理解:
const promise = getData(); // ① 调用 async 函数 → 拿到 Promise 对象(订单)
const food = await promise; // ② await 等订单完成 → 拆开包装 → 掏出 "饭"
// 合起来一行写:
const food = await getData(); // 等价于上面两步
// 关键:await 让 Promise 变回普通值——你拿到的不再是 Promise 对象,是 "饭" 本身
console.log(food); // "饭"
typeof food; // "string" —— 是字符串,不是 Promise 对象
一句话:
async函数是自动保鲜膜机器——你放什么进去,出来都包着 Promise。await就是反操作——把保鲜膜撕掉,掏出里面的东西。
async/await 三个规则(背下来):
| # | 规则 | 解释 |
|---|---|---|
| 1 | async function 永远返回 Promise | return "值" 会自动变成 Promise.resolve("值") |
| 2 | await 只能在 async 函数里用 | 普通函数里写 await 直接报语法错 |
| 3 | await 后面的代码 = 微任务 | 等价于 .then(() => { ... }) 里的回调 |
速记卡(面试闪卡)
Q1:一句话讲清「异步与事件循环」到底是什么?
A:事件循环(Event Loop)是 JS 单线程下调度异步回调的机制:耗时活儿交底层跑,跑完回主线程排队执行。
Q2:JS 单线程,为何需要异步? —— 怎么理解?
A:生活比喻:单线程像只有一个收银台,同一时刻只能结一单。同步(synchronous,按代码顺序一步步执行、前一步没完后面死等)就像排队买奶茶,前面不走你就干等;异步(asynchronous,把耗时活儿交给后台、好了再通知你)就像点完餐拿号,中间去逛街。浏览器除了 JS 引擎线程,还有渲染、网络、定时器线程——异步活儿是它们干的,干完把回调丢进队列等主线程空了来取。
Q3:宏任务与微任务,谁先跑? —— 怎么理解?
A:生活比喻:JS 有两个待办清单。宏任务(macrotask,如 setTimeout、I/O、UI 渲染,每次只取一个)像快递柜,来一件取一件;微任务(microtask,如 Promise.then、queueMicrotask,当前宏任务一结束就全部清空)像手边的急件,必须当场处理完。黄金规则:每跑完一个宏任务,先把所有微任务清空,再取下一个宏任务——所以 Promise.then 永远比 setTimeout(fn,0) 先执行。
Q4:Promise 是什么,构造器为何是同步的? —— 怎么理解?
A:生活比喻:Promise 像一个待定的外卖订单,状态从 pending(进行中)变 fulfilled(已送达)或 rejected(被拒收),.then 注册的回调进微任务队列。黄金陷阱:new Promise(fn) 里的 fn 是同步执行的!很多人以为“包了 Promise 里面就异步了”,其实 fn 当场就跑,只有 .then 回调才是异步的。所以耗时同步代码别直接塞构造器,要放进 setTimeout 或 async 函数。
Q5:async/await 是语法糖还是黑魔法? —— 怎么理解?
A:生活比喻:async/await 是 Promise 的语法糖(syntactic sugar,换更甜的写法、底层不变)——async 函数永远返回 Promise,await 像拆外卖包装,等 Promise 落定再继续,只是暂停这个函数、不阻塞主线程。它让异步代码读起来像同步,好调好查。await 后面跟非 Promise 会自动包成已完成的 Promise;await 所在的 async 函数本身仍是异步的,后面代码进了微任务。
Q6:核心速记主线有哪些?
-
单线程为何要异步:一个收银台,慢活儿不能堵路
-
宏任务后清空所有微任务:快递柜取一件、手边急件全处理
-
Promise 构造器是同步的:别把耗时同步代码塞进去
-
async/await 是语法糖:拆包装不挡道,函数暂停不阻塞
口诀
A:JS 单线程不阻塞,异步交给底层跑;
宏任务后清微任务,Promise.then 先报到;
new Promise 构造器,里面代码是同步;
async/await 是糖衣,暂停自身不挡道。
相关链接
-
📋 目录:00-JavaScript
-
📚 学习清单:八股文学习路线图
-
🔗 Python asyncio事件循环 — Python和JS的事件循环对比
-
🔗 IO多路复用 — JS事件循环底层依赖IO多路复用
-
🔗 React Fiber架构 — React Fiber与JS事件循环的配合