JS 是单线程语言,但前端要处理的定时器、点击事件、网络请求都是耗时的。怎么不卡死页面还能把这些事做完?答案是:Event Loop + Promise。
一、JS 为什么是单线程
1. 进程与线程
| 概念 | 类比 | 说明 |
|---|---|---|
| 进程 | 董事长 | 操作系统分配资源的基本单位,有独立 PID |
| 线程 | 经理 | 进程内执行代码的基本单位,一个进程可有多个线程 |
| 主线程 | 总经理 | 进程启动时自动创建的线程 |
| 子线程 | 副经理 | 主线程启动的其他线程 |
CPU 执行时间 :不能被一个程序霸占,操作系统用几十毫秒的轮询给每个进程分配执行时间片,看起来像多个程序同时跑。
2. 多线程 vs 单线程
| 语言 | 架构 | 特点 |
|---|---|---|
| C++ / Java | 多进程多线程 | 执行效率高,但复杂(锁、竞态、死锁) |
| JavaScript | 单线程 | 简单安全(操作 DOM 不会冲突),但处理耗时任务有挑战 |
JS 单线程的原因:最初设计用于浏览器操作 DOM,如果多线程同时改 DOM 会乱套。简单是它的设计哲学。
3. JS 的异步任务有哪些
单线程意味着一次只能做一件事,但下面这些任务都很耗时:
setTimeout定时器- 用户点击事件
fetch/axios网络请求- 文件读取、数据库查询(Node/Bun 场景)
如果用同步方式处理:等定时器跑完、等请求返回,整个页面就卡死了------按钮点不动、动画不动、页面假死。
怎么办? → 异步机制。
二、JS 执行机制:Event Loop
1. 进程和线程的启动
bash
前端 script 或后端 node/bun 代码执行
↓
启动一个进程(独立 PID,负责分配资源)
↓
进程启动一个主线程(JS 单线程,所有代码在这跑)
2. 同步优先,异步靠后
JS 的执行策略是:先把同步代码快速跑完,把耗时任务扔到 event loop,等主线程空了再回来处理。
javascript
console.log('start'); ← 1. 同步,立即执行
setTimeout(() => {
console.log('222'); ← 3. 异步,1 秒后执行
}, 1000);
console.log('end'); ← 2. 同步,立即执行
输出顺序 :start → end → 222
3. Event Loop 工作流程
vbnet
┌─────────────────────────────────────────┐
│ 主线程(同步代码) │
│ ┌───────────────────────────────────┐ │
│ │ 1. console.log('start') │ │
│ │ 2. setTimeout → 扔进 Event Loop │ │ ← 跳过,先往下执行
│ │ 3. console.log('end') │ │
│ │ 4. fetch → 扔进 Event Loop │ │ ← 跳过
│ │ 5. 同步代码全部执行完... │ │
│ └───────────────────────────────────┘ │
│ ↓ 主线程空闲 │
└──────────────────┼──────────────────────┘
↓
┌──────────────────────────────────────────┐
│ Event Loop(任务队列) │
│ - 检查定时器到没到时间 │
│ - 检查网络请求返回没 │
│ - 检查用户事件触发没 │
│ → 满足条件的回调,推回主线程执行 │
└──────────────────────────────────────────┘
核心 :JS 不是真的"同时"做很多事,而是把等待的活先挂着,先做能立刻完成的,等闲下来了再回来处理挂着的。这样页面就不会卡死,用户看到的同步代码(页面渲染、按钮响应)能快速展示。
三、为什么需要控制执行流程
异步虽然不卡死,但带来新问题:顺序不可控。
经典场景:先拿用户列表,再拿每个用户的详情
js
// A: 先 fetch 所有用户
fetch('/api/users')
.then(users => {
// B: 再 fetch 每个用户
// 如果 A 没完成,users 是 undefined,B 会报错
});
如果 A、B 都是异步的,且 B 依赖 A 的结果,就必须确保 A 先完成。这就是"控制执行流程"的需求。
JS 历史上有三种方案:
- 回调函数(callback hell,回调地狱)
- Promise(ES6,链式调用,本笔记重点)
- async/await(ES7,语法糖,最直观)
四、理解 Promise
1. 核心概念:一个"承诺"
Promise 直译就是"承诺"。你可以把它理解成一个未来会有结果的盒子:
- 现在盒子是空的 →
pending(进行中) - 将来兑现了 →
fulfilled(成功) - 将来毁约了 →
rejected(失败)
2. 实例化 Promise
js
const p = new Promise((resolve, reject) => {
// 这个函数叫 executor(执行器)
// 会立即执行,是耗时性任务的容器
console.log('许诺言'); // 同步,立即执行
setTimeout(() => {
// resolve 和 reject 是 Promise 给的两个函数
resolve(666); // 成功 → 调用 resolve
// reject("网络错误"); // 失败 → 调用 reject
}, 2000);
});
关键点
| 概念 | 说明 |
|---|---|
executor 函数 |
传给 new Promise 的函数,立即同步执行 |
| 容器作用 | executor 里可以放异步任务(如 setTimeout) |
resolve 函数 |
表示"成功解决了",手动调用 |
reject 函数 |
表示"失败了",手动调用 |
3. 三个状态 + 三个回调
js
p
.then((data) => { // resolve 了走 then,data 是 resolve 传的参数
console.log(data); // 666
})
.catch((err) => { // reject 了走 catch,err 是 reject 传的参数
console.log(err); // "网络错误"
})
.finally(() => { // 无论成功失败,都会执行
console.log('finally');
});
4. 数据传递流程
scss
resolve(result)
↓
└→ .then(data => { ... }) // data = result
reject(err)
↓
└→ .catch(error => { ... }) // error = err
关键 :resolve(666) 里的 666,会作为 .then(data => ...) 中 data 的值;reject("网络错误") 里的字符串,会作为 .catch(err => ...) 中 err 的值。
五、实战代码拆解
1. 同步 vs 异步执行顺序
js
console.log('start'); // 同步
setTimeout(() => { // 异步
console.log('222');
}, 1000);
console.log('end'); // 同步
// 输出:start → end → 222
验证 Event Loop :setTimeout 被扔进任务队列,主线程先跑完 end,1 秒后定时器触发,才把 222 推回主线程执行。
2.多线程假想
js
// 如果是多线程,可以 3 个线程分别声明 a, b, c,并发执行
// 缺点是复杂(要处理线程同步、锁)
let a = 1; // 单线程顺序执行
let b = 2;
let c = 3;
console.log(a + b + c); // 6
对比说明:单线程代码简单直接,但处理耗时任务要靠异步机制。
3. Promise 完整流程
js
const p = new Promise((resolve, reject) => {
console.log('许诺言'); // 1. 立即同步执行
setTimeout(() => {
// resolve(666); // 成功路径
reject("网络错误"); // 失败路径(演示)
}, 2000);
});
console.log(p.__proto__); // 2. 同步,看 Promise 的原型
p
.then((data) => { // 3. 成功时执行
console.log(data);
console.log('end');
})
.catch((err) => { // 4. 失败时执行
console.log(err); // "网络错误"
console.log('失败了');
})
.finally(() => { // 5. 无论成功失败都执行
console.log('finally');
});
输出(reject 路径):
javascript
许诺言 ← 立即
Promise { ... } ← 立即(原型对象)
(等 2 秒)
网络错误 ← 2 秒后
失败了
finally
4. fetch 底层就是 Promise
js
console.log('start');
fetch('https://api.github.com/chat/completions', {
method: 'POST'
})
.then((data) => { /* 请求成功 */ })
.catch((err) => { console.log(err); });
console.log('end');
// 输出顺序:start → end → (请求返回后) then 或 catch
说明 :fetch 本身返回一个 Promise,所以能直接 .then().catch()。所有现代异步 API(fetch、axios、数据库驱动)都是基于 Promise 的。
5. 用 Promise 封装 sleep
js
function sleep(t) {
const p = new Promise((resolve, reject) => {
console.log('同步'); // 调用 sleep 时立刻打印
setTimeout(() => {
resolve(); // 这个回调才是异步,t 毫秒后才执行,表示"承诺兑现"
}, t);
});
return p; // 返回 Promise,
}
sleep(2000)
.then(() => {
console.log('2s 后再做');
});
关键点 1: new Promise(...) 里的代码是立即同步执行 的,Promise 构造函数传进去的那个函数,会当场执,不是异步。所以:
- console.log('同步') → 立即 执行
- setTimeout(..., t) → 启动 定时器(立即返回),回调要 t 毫秒后才跑
- resolve() → t 毫秒后定时器触发时才调用,表示"承诺兑现"
关键点 2: return p 的作用: sleep 函数返回这个 Promise,让外面能 .then() 它。
为什么 JS 不内置 sleep:单线程同步 sleep 会卡死整个页面 2 秒。用 Promise 包 setTimeout,是"模拟延迟"的标准做法,不会阻塞主线程。
六、三种写法对比(同一件事)
| 写法 | 代码 | 特点 |
|---|---|---|
| 回调函数 | sleep(2000, () => { ... }) |
嵌套深,回调地狱 |
| Promise .then() | sleep(2000).then(() => { ... }) |
链式调用,可读 |
| async/await | await sleep(2000); ... |
像同步代码,最直观 |
三者关系:async/await 是 Promise 的语法糖,Promise 是回调函数的升级版。
js
// Promise 写法
sleep(2000).then(() => {
console.log('2s 后再做');
});
// async/await 写法(等价)
async function main() {
await sleep(2000);
console.log('2s 后再做');
}
main();
七、Promise 的本质:状态机
kotlin
pending(进行中)
/ \
resolve reject
/ \
↓ ↓
fulfilled rejected
(已兑现) (已毁约)
│ │
.then(data) .catch(err)
│ │
└──→ .finally() ←────┘
关键特性:
- 状态不可逆:从 pending 变成 fulfilled 或 rejected 后,就固定了,不能再变
- 只认第一次:executor 里无论调多少次 resolve/reject,只有第一次生效
- 值穿透:resolve 传的值,会一路传到 .then;reject 传的值,会一路传到 .catch
js
const p = new Promise((resolve, reject) => {
resolve('A');
resolve('B'); // ❌ 无效,已经 resolve 过了
reject('C'); // ❌ 无效
});
p.then(data => console.log(data)); // 'A'
八、总结
核心知识点
- JS 是单线程:简单安全,但要靠异步机制处理耗时任务
- Event Loop:同步代码优先执行,异步任务扔进队列,主线程空闲时再处理
- Promise 三状态:pending → fulfilled / rejected
- executor 立即同步执行 :
new Promise(函数)里的函数是同步的,但里面可以放异步任务 - 数据传递 :
resolve(data)的 data →.then(data);reject(err)的 err →.catch(err) - fetch / axios 底层都是 Promise:现代异步 API 的标准接口
心法
异步不是"同时做",而是"先挂着,闲了再做"。Promise 是装"未来结果"的盒子,
.then等兑现,.catch等毁约,.finally最后都要执行。
选型建议
- 简单异步 :
setTimeout+ 回调够用 - 有依赖链 :Promise
.then().catch() - 多步异步且要顺序执行 :
async/await(最直观) - 多个异步并发等全部完成 :
Promise.all([p1, p2, p3]) - 多个异步只要有一个完成 :
Promise.race([p1, p2, p3])