一,同步和异步
1.1 生活类比
同步(Synchronous):你去食堂打饭,排队时你死盯着窗口,前面的人不端走,你就不动,也不玩手机。你“阻塞”了。
异步(Asynchronous):你去食堂打饭,拿了个叫号振动器。排队时你回座位刷手机,振动器响了再去取餐。你“非阻塞”,系统效率更高。
1.2 代码demo
同步代码
console.log('1. 开始做饭');
function cookRice() {
// 假设煮饭需要 3 秒(这里用循环模拟耗时,会卡住)
let start = Date.now();
while (Date.now() - start < 3000) {} // 死循环 3 秒
console.log('2. 饭煮好了');
}
cookRice();
console.log('3. 可以炒菜了');
输出顺序(铁定不变):
1. 开始做饭
(卡顿 3 秒)
2. 饭煮好了
3. 可以炒菜了
第 3 行必须等煮饭完成才能执行,页面会卡死。
异步代码
console.log('1. 开始做饭');
setTimeout(() => {
console.log('2. 饭煮好了(异步通知)');
}, 3000); // 3秒后执行
console.log('3. 可以炒菜了(不用等饭)');
输出顺序
1. 开始做饭
3. 可以炒菜了(不用等饭)
(约 3 秒后)
2. 饭煮好了(异步通知)
第 3 行不等煮饭,先炒菜,饭好了再回来处理。
1.3 核心结论
| 维度 | 同步 (Sync) | 异步 (Async) |
|---|---|---|
| 执行顺序 | 从上到下,一行等一行 | 耗时任务先“挂起”,先执行后面的代码 |
| 是否阻塞 | 阻塞(卡住后面的代码) | 非阻塞(后面的代码照常运行) |
| 返回值 | 直接拿到结果 | 通过回调/Promise 等“将来”拿结果 |
| 典型场景 | 普通计算、数组遍历 | 网络请求、定时器、文件读取、事件监听 |
| 对用户影响 | 页面卡顿(白屏) | 页面流畅(体验好) |
二,回调函数
2.1 回调函数到底是什么
定义:把一个函数作为参数传给另一个函数,在未来某个时刻被调用执行。
生活类比:你去奶茶店点单,店员给你一个号码牌说:“等叫号的时候,你凭牌来取”。
- 这个“取奶茶的动作”就是回调函数,叫号的时候才会执行。
2.2 回调的两种常见形态
同步回调
function greet(name, callback) {
callback(name); // 立即调用
}
greet('小明', (name) => {
console.log(`你好,${name}`);
});
// 输出:你好,小明(立即)
这个只是函数传参,不涉及异步
异步回调
console.log('1. 开始点单');
// setTimeout 就是异步回调的典型
setTimeout(() => {
console.log('2. 奶茶做好了(回调执行)');
}, 2000);
console.log('3. 先去看会儿手机');
输出:
1. 开始点单
3. 先去看会儿手机
(2秒后)
2. 奶茶做好了(回调执行)
() => { ... } 这个箭头函数就是回调函数,它被放在未来(2 秒后)执行。
2.3 常见的异步回调场景
| 场景 | 示例代码 |
|---|---|
| 定时器 | setTimeout(() => {}, 1000) |
| DOM 事件 | btn.addEventListener('click', () => {}) |
| Node.js 读文件 | fs.readFile('a.txt', (err, data) => {}) |
| 旧式 Ajax | $.get('/api', (res) => {}) |
2.4 回调地狱
当你有多个异步任务必须按顺序执行(比如:先读 A 文件 → 再读 B 文件 → 再提交数据),回调就会层层嵌套,变成“三角形代码”。
回调地狱现场
// 伪代码:先登录 → 获取用户信息 → 获取订单 → 获取订单详情
login('user', 'pass', (err, token) => {
if (err) throw err;
getUserInfo(token, (err, user) => {
if (err) throw err;
getOrders(user.id, (err, orders) => {
if (err) throw err;
getOrderDetail(orders[0].id, (err, detail) => {
if (err) throw err;
console.log('最终详情:', detail);
// 如果再嵌套一层,就变成“金字塔”了
});
});
});
});
回调地狱的 4 大问题
- 代码横向膨胀,可读性极差(像圣诞树)。
- 错误处理重复,每个层级都要
if (err)。 - 难以维护,改一个逻辑要动多层。
- 无法直接使用
return/try-catch,控制流被割裂。
三,事件循环
3.1 JS 为什么需要事件循环
核心前提:
- JS 是单线程(只有一个主线程干活)。
- 但浏览器/Node 环境提供了 Web APIs(定时器、HTTP请求、DOM事件)来帮我们做耗时任务。
- 当这些“帮手”完成任务后,需要把回调函数排好队,等主线程空闲了再执行。
- 事件循环(Event Loop) 就是那个不停看“主线程是否空闲”的调度员。
生活类比:地铁调度
把 JS 运行机制想象成一个地铁站:
| 概念 | 类比 |
|---|---|
| 主线程(调用栈) | 地铁进站口,一次只能通过一个人 |
| 宏任务队列 | 普通乘客通道(排队等进站) |
| 微任务队列 | VIP 快速通道(永远插队到普通通道前面) |
| 事件循环 | 地铁调度员,不停检查进站口有没有人,没人就从队列拉人进来 |
规则:
- 先执行完当前所有的同步代码(清空进站口)。
- 然后清空整个微任务队列(VIP 全放行)。
- 最后只取一个宏任务来执行(放一个普通乘客)。
- 执行这个宏任务时,如果又产生了新的微任务,继续清空微任务。
- 重复以上循环。
先同步,再微任务,最后宏任务(一次只取一个),微任务清空后,才取下一个宏任务
3.2 宏任务 vs 微任务
| 类型 | 包含哪些 API |
|---|---|
| 宏任务(MacroTask) | setTimeout、setInterval、setImmediate、I/O 操作、UI 渲染 |
| 微任务(MicroTask) | Promise.then()、Promise.catch()、MutationObserver、process.nextTick(Node) |
注意:
Promise本身是同步的,但then/catch/finally里的回调是微任务。
四,定时器
4.1 setTimeout
setTimeout 不保证精确延迟,它只保证“至少等待指定时间后执行”。
也就是说:你写 setTimeout(fn, 1000),意思是 “最早 1 秒后执行”,而不是 “1 秒整执行”。
4.2 为什么不精准?
主线程被阻塞 是罪魁祸首。
代码演示
console.log('1. 设置定时器,延迟 100ms');
setTimeout(() => {
console.log('2. 定时器回调执行了');
}, 100);
// 模拟一个耗时 500ms 的同步任务(阻塞主线程)
let start = Date.now();
while (Date.now() - start < 500) {
// 死循环,啥也不干
}
console.log('3. 耗时任务结束');
实际输出顺序:
1. 设置定时器,延迟 100ms
3. 耗时任务结束 (约 500ms 后输出)
2. 定时器回调执行了 (紧接着输出)
发生了什么?
- 第 1 行:
setTimeout注册,计时器在后台开始倒计时(100ms)。 - 第 100ms 时:定时器到期,回调被放入宏任务队列等待执行。
- 但此时主线程正在执行那个
while循环(还没结束!),所以回调在队列里干等。 - 第 500ms 时:
while结束,主线程空闲。 - 主线程检查宏任务队列,拿出定时器回调执行。
结果:明明写 100ms,实际却等了 500ms+。
4.3 最小延迟限制
即使你写 setTimeout(fn, 0),浏览器也不会真的 0ms 执行。
规范规定:
- 浏览器环境:嵌套层级 ≥ 5 时,最小延迟为 4ms
- Node.js 环境:最小延迟为 1ms
演示代码
let start = Date.now();
function nestedTimeout() {
if (Date.now() - start > 50) {
console.log('实际耗时:', Date.now() - start, 'ms');
return;
}
setTimeout(nestedTimeout, 0);
}
nestedTimeout();
你会发现实际耗时远大于 0ms,因为浏览器强制插入了 4ms 的“节流”。
4.4 两个替代方案
| 方案 | 代码 | 特点 |
|---|---|---|
| queueMicrotask | queueMicrotask(() => console.log('尽快')) | 微任务,当前宏任务结束后立即执行 |
| Promise.resolve().then | Promise.resolve().then(() => console.log('尽快')) | 同样是微任务,用法更常见 |
对比演示
console.log('1');
setTimeout(() => console.log('2 (setTimeout)'), 0);
queueMicrotask(() => console.log('3 (微任务)'));
Promise.resolve().then(() => console.log('4 (微任务)'));
console.log('5');
输出:
1
5
3
4
2 (setTimeout)
queuMicrotask 和 Promise.then 都在宏任务之前执行,真正做到了“尽快”。
4.5 实际开发中的痛点场景
场景 1:动画卡顿
// 错误:用 setTimeout 做动画(不准,会掉帧)
function animate() {
moveBox();
setTimeout(animate, 16); // 想 60fps,但不准
}
// 正确:用 requestAnimationFrame(由浏览器刷新率驱动)
function animate() {
moveBox();
requestAnimationFrame(animate);
}
场景 2:倒计时不准
// 倒计时会越走越慢
let count = 10;
setInterval(() => {
count--;
console.log(count);
}, 1000);
// 如果用户切到后台,浏览器会降低定时器频率,倒计时会滞后
// 用时间戳修正
let targetTime = Date.now() + 10 * 1000;
function updateCountdown() {
let remaining = targetTime - Date.now();
if (remaining <= 0) return;
console.log(Math.ceil(remaining / 1000));
requestAnimationFrame(updateCountdown);
}
五,Promise
5.1 什么是Promise?
Promise 就像“外卖订单”:
| 外卖订单状态 | Promise 状态 | 含义 |
|---|---|---|
| 已下单,等骑手接单 | Pending(进行中) | 异步任务还在执行,结果未知 |
| 骑手已取餐,正在配送 | Fulfilled(已完成) | 异步任务成功,拿到了结果(你的外卖) |
| 商家爆单,订单取消 | Rejected(已拒绝) | 异步任务失败,得到了错误原因 |
关键特性:
- 状态只能改变一次:要么 Pending → Fulfilled,要么 Pending → Rejected。
- 一旦状态确定,永远不变(不可逆)。
- 你可以在任何时候(状态确定之前或之后)去“获取”结果。
5.2 Promise 的基本用法
1. 创建一个 Promise
const promise = new Promise((resolve, reject) => {
// 这里放异步任务(比如定时器、网络请求)
setTimeout(() => {
const success = true;
if (success) {
resolve('外卖送到了!'); // 成功 → 状态变为 Fulfilled
} else {
reject('商家取消订单了 '); // 失败 → 状态变为 Rejected
}
}, 1000);
});
解读:
new Promise构造函数接收一个执行器函数(executor),它立即执行(同步)。- 执行器函数有两个参数:
resolve和reject,它们是 JS 引擎提供的函数。 - 调用
resolve(value)→ 状态变为Fulfilled,value就是结果。 - 调用
reject(reason)→ 状态变为Rejected,reason就是错误原因。
2. 消费 Promise
promise
.then((result) => {
console.log('成功:', result);
})
.catch((error) => {
console.log('失败:', error);
})
.finally(() => {
console.log('无论成功失败,都会执行');
});
输出(1 秒后):
成功: 外卖送到了!
无论成功失败,都会执行
3. 链式调用
then() 方法返回一个新的 Promise,这是链式调用的基础。
const promise = new Promise((resolve) => {
resolve(1);
});
promise
.then((value) =>
console.log('第一次:', value); // 1
return value + 1; // 返回 2(会被包装成 Promise)
})
.then((value) => {
console.log('第二次:', value); // 2
return value + 1;
})
.then((value) => {
console.log('第三次:', value); // 3
});
输出:
第一次: 1
第二次: 2
第三次: 3
核心规则:
- 在
then中 return 一个普通值 → 会被自动包装成Promise.resolve(值) - 在
then中 return 一个 Promise → 会等待这个 Promise 完成 - 在
then中 抛出一个错误(throw) → 会触发最近的.catch()
4. 错误处理
// 写法一:用 .catch(推荐)
promise
.then((data) => console.log(data))
.catch((err) => console.error(err));
// 写法二:then 的第二个参数
promise
.then(
(data) => console.log(data),
(err) => console.error(err)
);
区别:
.catch能捕获前面所有 then 中抛出的错误。then的第二个参数只能捕获当前 Promise 的错误。
5.3 Promise 的静态方法
| 方法 | 作用 |
|---|---|
Promise.resolve(value) | 直接返回一个成功状态的 Promise |
Promise.reject(reason) | 直接返回一个失败状态的 Promise |
Promise.all([p1, p2]) | 全部成功才成功,任何一个失败则立即失败 |
Promise.race([p1, p2]) | 谁先完成(成功或失败)就返回谁 |
代码示例
// Promise.resolve 快速创建成功 Promise
Promise.resolve('直接成功')
.then(console.log); // 直接成功
// Promise.all 并发执行
Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]).then((results) => {
console.log(results); // [1, 2, 3]
});
// 如果有一个失败
Promise.all([
Promise.resolve(1),
Promise.reject('出错了'),
Promise.resolve(3)
]).catch((err) => {
console.log(err); // 出错了(立即失败,不会等第三个)
});
5.4 Promise 解决了什么?
| 回调的痛点 | Promise 的解决方案 |
|---|---|
| 回调地狱(嵌套金字塔) | 链式调用(.then().then()) |
错误处理分散(每层 if(err)) | 统一错误捕获(.catch()) |
| 控制流割裂(不能 return) | 可以 return 值,流程更自然 |
| 难以组合多个异步 | Promise.all / race 轻松并发 |
对比代码
// 回调地狱
login('user', 'pass', (err, token) => {
if (err) throw err;
getUserInfo(token, (err, user) => {
if (err) throw err;
getOrders(user.id, (err, orders) => {
if (err) throw err;
console.log(orders);
});
});
});
// Promise 链式(扁平化)
login('user', 'pass')
.then(token => getUserInfo(token))
.then(user => getOrders(user.id))
.then(orders => console.log(orders))
.catch(err => console.error(err));
六,async/await
6.1 async/await是什么
async/await是 Promise 的语法糖,它让异步代码看起来像同步代码,底层依然是 Promise + 微任务。
本质:
async函数永远返回一个 Promiseawait会暂停函数执行,等待 Promise 状态确定后继续await只能在async函数中使用
6.2 生活类比
回调 /Promise 方式:你点完奶茶,拿到号牌(Promise),然后可以到处逛,等振动器响了再回来取(.then)。
async/await 方式:你点完奶茶,站在柜台前等着(await),店员做好直接递给你,你拿了就走。代码看起来像 "排队等着",但实际上并没有阻塞主线程(JS 引擎帮你做了 "暂停和恢复")。
6.3 基础语法
对比 1:单个异步任务
Promise 写法:
function fetchUser() {
return new Promise((resolve) => {
setTimeout(() => resolve('小明'), 1000);
});
}
fetchUser().then((user) => {
console.log(user); // 小明
});
async/await 写法:
function fetchUser() {
return new Promise((resolve) => {
setTimeout(() => resolve('小明'), 1000);
});
}
async function getUser() {
const user = await fetchUser(); // 等待 1 秒
console.log(user); // 小明
}
getUser();
关键区别:
await让代码看起来像"同步等待",但不会阻塞主线程await后面的代码被隐式包装成.then()回调
对比 2:多个顺序依赖的任务
Promise 链式:
function login() { return Promise.resolve('token'); }
function getUser(token) { return Promise.resolve({ id: 1, name: '小明' }); }
function getOrders(userId) { return Promise.resolve(['订单1', '订单2']); }
login()
.then(token => getUser(token))
.then(user => getOrders(user.id))
.then(orders => console.log(orders))
.catch(err => console.error(err));
async/await 写法(完胜):
async function getOrderList() {
try {
const token = await login();
const user = await getUser(token);
const orders = await getOrders(user.id);
console.log(orders);
} catch (err) {
console.error(err);
}
}
getOrderList();
逻辑顺序从上到下,一目了然,没有任何嵌套!
6.4 核心机制拆解
看这段代码:
async function demo() {
console.log('1');
const result = await someAsyncTask();
console.log('2');
}
demo();
console.log('3');
实际执行顺序:
1
3
(等待 someAsyncTask 完成)
2
底层发生了什么?(重要!)
// 上面的 async/await 等价于:
function demo() {
console.log('1');
return someAsyncTask().then((result) => {
console.log('2');
});
}
demo();
console.log('3');
关键结论:
await后面的代码,相当于被放进了.then()回调里。await会让出主线程,所以后面的同步代码(console.log('3'))会先执行。async函数本身会立即执行到第一个await处,然后返回一个 Pending 状态的 Promise。
6.5 错误处理
方式一:try-catch
async function fetchData() {
try {
const data = await fetch('/api/data');
const json = await data.json();
console.log(json);
} catch (err) {
console.error('请求失败:', err.message);
}
}
方式二:.catch
async function fetchData() {
const data = await fetch('/api/data').catch(err => {
console.error('请求失败:', err);
return null; // 返回默认值
});
if (data) {
const json = await data.json();
console.log(json);
}
}
核心规则:
await后面 Promise 被reject了,会抛出异常,可以用try-catch捕获。- 如果多个
await,一个try-catch就能捕获所有错误。 - 如果不想整个函数中断,可以在单个
await后面加.catch()。
6.6 关键注意点
坑 1:忘记加 await
// 错误:没有 await
async function getData() {
const result = fetch('/api'); // result 是 Promise,不是数据
console.log(result); // Promise { <pending> }
}
// 正确
async function getData() {
const result = await fetch('/api');
console.log(result); // 实际数据
}
坑 2:在顶层作用域使用 await(ES2022 已支持)
旧方式(需要包装):
// 旧语法:顶层不能用 await
const data = await fetch('/api'); // SyntaxError
// 必须包装在 async 函数里
(async () => {
const data = await fetch('/api');
console.log(data);
})();
新方式(ES2022 模块顶层 await):
// 在 ES Module 中可以直接用(需 type="module")
const data = await fetch('/api');
console.log(data);
坑 3:并发错误(用 Promise.all 优化)
// 慢:顺序执行,总耗时 2 秒
async function getData() {
const user = await fetchUser(); // 等待 1 秒
const orders = await fetchOrders(); // 再等待 1 秒
return { user, orders };
}
// 快:并发执行,总耗时 1 秒
async function getData() {
const [user, orders] = await Promise.all([
fetchUser(),
fetchOrders()
]);
return { user, orders };
}
原则:没有依赖关系的异步任务,用 Promise.all 并发执行!
七,异常处理
7.1 结论
在
async/await中用try-catch,在 Promise 链中用.catch()。
两种方式核心原理一样,但使用场景和细节不同。
7.2 两种方式的本质对比
| 维度 | try-catch | .catch() |
|---|---|---|
| 适用场景 | async/await 写法 | Promise 链式写法 |
| 捕获范围 | 捕获 await 抛出的错误 | 捕获链中前面所有 then 的错误 |
| 是否阻塞 | 会中断 try 块后续代码 | 会跳过错误后的 then,跳到 catch |
| 错误恢复 | 在 catch 块中处理,然后继续 | catch 返回新 Promise,后续 then 继续 |
| 可读性 | 更接近同步代码 | 链式风格 |
7.3 场景 1:async/await 中的错误处理
标准写法(推荐)
async function fetchUserData() {
try {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
console.log('用户数据:', data);
return data;
} catch (err) {
console.error('获取用户数据失败:', err.message);
// 错误恢复:返回一个默认值
return { name: '默认用户', id: -1 };
}
}
// 调用方
const user = await fetchUserData();
console.log('最终结果:', user);
关键点:
try块中任何一个await出错,都会跳到catch。catch中可以返回默认值、重试、或者重新抛出错误。- 错误恢复后,函数正常返回,调用方不会收到异常。
单个 await 独立错误处理(细粒度控制)
async function complexTask() {
// 任务 A 失败不影响任务 B
let user = null;
try {
user = await fetchUser();
} catch (err) {
console.warn('获取用户失败,使用默认用户');
user = { name: '游客' };
}
// 任务 B 失败则整体失败
const orders = await fetchOrders(user.id);
return { user, orders };
}
重新抛出错误(向上传递)
async function login() {
try {
const result = await doLogin();
return result;
} catch (err) {
// 记录日志后,重新抛出
console.error('登录失败,已记录日志');
throw new Error('登录失败,请稍后重试');
}
}
// 调用方必须处理这个错误
try {
await login();
} catch (err) {
showToast(err.message);
}
7.4 场景 2:Promise 链中的错误处理
标准写法
fetch('/api/user')
.then(response => {
if (!response.ok) throw new Error('请求失败');
return response.json();
})
.then(data => {
console.log('用户数据:', data);
return data;
})
.catch(err => {
console.error('错误:', err.message);
return { name: '默认用户' }; // 错误恢复
});
关键点:
.catch()捕获前面所有.then()中抛出的错误。.catch()返回一个新 Promise,后续的.then()会继续执行。
多个独立任务的错误隔离
// 错误:一个失败全部失败
Promise.all([
fetchUser(),
fetchOrders(),
fetchSettings()
]).catch(err => {
console.error('某个任务失败:', err);
});
// 正确:每个任务独立处理
Promise.all([
fetchUser().catch(err => ({ name: '默认用户' })),
fetchOrders().catch(err => []),
fetchSettings().catch(err => ({ theme: 'light' }))
]).then(([user, orders, settings]) => {
console.log(user, orders, settings);
});
7.5 try-catch 无法捕获什么?
1. 未 await 的 Promise 错误
// 错误:没有 await,try-catch 捕获不到
async function bad() {
try {
Promise.reject('出错了'); // 没有 await,异步执行
} catch (err) {
console.log('捕获不到!'); // 不会执行
}
}
// 会触发 UnhandledPromiseRejection
// 正确:必须 await
async function good() {
try {
await Promise.reject('出错了');
} catch (err) {
console.log('捕获到了:', err); // 捕获到了
}
}
2. 异步回调中的错误
// 错误:setTimeout 回调中的错误捕获不到
async function bad() {
try {
setTimeout(() => {
throw new Error('回调中的错误');
}, 0);
} catch (err) {
console.log('捕获不到!'); // 不会执行
}
}
// 正确:用 Promise 包装
function delayError() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('回调中的错误'));
}, 0);
});
}
async function good() {
try {
await delayError();
} catch (err) {
console.log('捕获到了:', err.message);
}
}
7.6 全局错误兜底
浏览器环境
// 捕获未处理的 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 错误:', event.reason);
// 可以上报到监控系统
reportError(event.reason);
});
// 捕获未处理的同步错误
window.addEventListener('error', (event) => {
console.error('未处理的错误:', event.message);
});
Node.js 环境
// 捕获未处理的 Promise 错误
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 错误:', reason);
});
// 捕获未处理的同步错误
process.on('uncaughtException', (err) => {
console.error('未捕获的异常:', err);
// 注意:不要在这里重启进程,而是记录后退出
process.exit(1);
});
7.7 实战最佳实践
| # | 实践 | 说明 |
|---|---|---|
| 1 | 永远给 await 加上 try-catch | 尤其是网络请求、文件操作 |
| 2 | catch 后要返回合理默认值 | 避免整个流程中断 |
| 3 | 需要上层处理的错误要 throw | 不要吞掉错误 |
| 4 | 并发任务用 Promise.all + 独立 .catch | 避免一个失败全盘崩溃 |
| 5 | 添加全局 unhandledrejection 兜底 | 作为最后防线 |
八,并发控制
8.1 总览对比
| 方法 | 返回条件 | 失败行为 | 适用场景 |
|---|---|---|---|
| Promise.all | 全部成功 | 一个失败立即失败 | 所有任务必须全部成功 |
| Promise.race | 任意一个完成 | 失败算完成 | 超时控制、竞速 |
| Promise.allSettled | 全部完成(无论成败) | 不会失败 | 需要所有结果,允许部分失败 |
| Promise.any | 任意一个成功 | 全部失败才失败 | 多个备用源,取最快成功的 |
8.2 逐个拆解
Promise.all
场景:加载用户资料、订单列表、权限配置,缺一个页面都不能展示。
function fetchUser() {
return new Promise(resolve => setTimeout(() => resolve('用户数据'), 500));
}
function fetchOrders() {
return new Promise(resolve => setTimeout(() => resolve('订单数据'), 800));
}
function fetchSettings() {
return new Promise(resolve => setTimeout(() => resolve('设置数据'), 300));
}
// 并发执行,总耗时 = 最慢的那个(800ms),而不是 500+800+300=1600ms
async function loadDashboard() {
try {
const [user, orders, settings] = await Promise.all([
fetchUser(),
fetchOrders(),
fetchSettings()
]);
console.log('全部加载完成:', { user, orders, settings });
// 渲染页面...
} catch (err) {
console.error('某个数据加载失败,整个页面无法展示');
showErrorPage();
}
}
关键特性:
- 输入数组任何一个被 reject,
Promise.all立即 reject。 - 结果顺序保持与输入一致(不管谁先完成)。
- 总耗时 = 最慢那个任务的时间。
Promise.race
场景:超时控制、多个 CDN 地址抢最快加载。
场景一:超时控制(最常用)
function fetchWithTimeout(url, timeout = 3000) {
return Promise.race([
fetch(url), // 正常请求
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
]);
}
// 使用
try {
const response = await fetchWithTimeout('/api/slow', 2000);
const data = await response.json();
console.log(data);
} catch (err) {
console.log('请求失败或超时:', err.message);
}
场景二:多个数据源择优
function queryCDN(cdnName, delay) {
return new Promise(resolve => {
setTimeout(() => resolve(`${cdnName} 返回数据`), delay);
});
}
const result = await Promise.race([
queryCDN('阿里云', 1000),
queryCDN('腾讯云', 500), // 这个最快
queryCDN('华为云', 1500)
]);
console.log(result); // "腾讯云 返回数据"
关键特性:
- 输入数组任意一个状态改变(成功或失败),
race立即返回。 - 其他任务不会取消,只是被忽略(会继续执行但结果被丢弃)。
- 常用于超时保护。
Promise.allSettled
场景:批量上传文件,失败的不影响成功;批量埋点,部分失败不阻塞主流程。
const uploadTasks = [
uploadFile('image1.jpg'),
uploadFile('image2.jpg'),
uploadFile('image3.jpg'), // 这个可能失败
];
const results = await Promise.allSettled(uploadTasks);
// 结果格式
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`文件 ${index + 1} 上传成功:`, result.value);
} else {
console.log(`文件 ${index + 1} 上传失败:`, result.reason);
}
});
// 筛选出成功和失败
const successful = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const failed = results.filter(r => r.status === 'rejected').map(r => r.reason);
返回值格式:
// 成功:{ status: 'fulfilled', value: '结果' }
// 失败:{ status: 'rejected', reason: '错误' }
关键特性:
- 永远不会 reject,永远 resolve。
- 适合"部分成功可接受"的场景。
- ES2020 引入,旧环境需要 polyfill。
Promise.any
场景:多个备用服务器,哪个能用用哪个;多个广告接口,取最快成功的。
function requestServer(serverName, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(`${serverName} 不可用`);
} else {
resolve(`${serverName} 响应成功`);
}
}, Math.random() * 1000);
});
}
try {
const result = await Promise.any([
requestServer('服务器A', false),
requestServer('服务器B', true), // 失败
requestServer('服务器C', true), // 失败
]);
console.log('最快可用的服务器:', result);
} catch (err) {
// 只有全部失败才会进入这里
console.log('所有服务器都不可用');
}
关键特性:
- 只要一个成功就 resolve。
- 全部失败才 reject,错误是一个
AggregateError(包含所有错误)。 - ES2021 引入,较新特性。
8.3 实战对比
场景:请求三个不同的推荐数据源。
const source1 = fetch('/api/recommend/1').catch(() => null);
const source2 = fetch('/api/recommend/2').catch(() => null);
const source3 = fetch('/api/recommend/3').catch(() => null);
| 需求 | 最佳方案 |
|---|---|
| 三个都必须成功才能展示 | Promise.all([source1, source2, source3]) |
| 任意一个成功就展示 | Promise.any([source1, source2, source3]) |
| 全部展示,失败的显示兜底 | Promise.allSettled([source1, source2, source3]) |
| 2秒内哪个先返回用哪个 | Promise.race([Promise.all(...), timeout]) |
8.4 常见陷阱
陷阱 1:Promise.all 中一个失败不会等其他人
Promise.all([
Promise.resolve(1),
Promise.reject('错误'),
Promise.resolve(3) // 这个不会执行完,因为上面已经失败了
]).catch(err => console.log(err)); // "错误"
陷阱 2:race 不取消其他任务(内存泄漏风险)
const result = await Promise.race([
fetch('/api/slow'), // 这个请求如果很慢,即使 race 返回了
new Promise(resolve => setTimeout(() => resolve('超时'), 1000))
]);
// 慢请求依然在后台执行,只是结果被丢弃
// 实际项目中需要用 AbortController 取消
陷阱 3:any 和 race 的混淆
// race:第一个状态改变就返回(可能是失败)
// any:第一个成功的返回;全部失败才失败
Promise.race([
Promise.reject('A'),
Promise.resolve('B')
]); // 返回 'B'(因为 B 先成功)
Promise.any([
Promise.reject('A'),
Promise.resolve('B')
]); // 返回 'B'
8.5 总结
| # | 场景 | 推荐方案 |
|---|---|---|
| 1 | 页面初始化加载多个模块 | Promise.all + 全局 Loading |
| 2 | 请求超时控制 | Promise.race + setTimeout |
| 3 | 批量操作(部分失败可接受) | Promise.allSettled |
| 4 | 多备用服务器/数据源容灾 | Promise.any |
| 5 | 大量并发任务(几十上百个) | 用 p-limit 或 p-lazy 控制并发数 |
好!你直接点到了进阶核心,我一次性把这两个讲透。
九,手写 Promise
9.1 先搭骨架
一个 Promise 的核心就三样东西:
- 状态(pending / fulfilled / rejected)
- 值(成功结果或失败原因)
- then 方法(注册回调)
class MyPromise {
constructor(executor) {
this.state = 'pending'; // 初始状态
this.value = undefined; // 存放结果
this.reason = undefined; // 存放失败原因
this.onFulfilledCallbacks = []; // 成功回调队列
this.onRejectedCallbacks = []; // 失败回调队列
// resolve 函数
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
// 执行所有注册的成功回调
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
// reject 函数
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
// 处理值穿透:如果没传回调,默认传递值
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
// 关键:then 必须返回一个新的 Promise
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
// 用微任务模拟(实际规范是微任务,这里用 setTimeout 模拟)
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (err) {
reject(err);
}
});
}
if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (err) {
reject(err);
}
});
}
if (this.state === 'pending') {
// 还没完成,先把回调存起来
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (err) {
reject(err);
}
});
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (err) {
reject(err);
}
});
});
}
});
return promise2;
}
// 静态方法:快速创建成功/失败 Promise
static resolve(value) {
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}
9.2 测试
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('成功!');
}, 1000);
});
p.then((value) => {
console.log('结果:', value);
return '链式传递';
}).then((value) => {
console.log('链式:', value);
});
// 输出(1秒后):
// 结果: 成功!
// 链式: 链式传递
9.3 核心难点
上面代码有个问题:如果 onFulfilled 返回一个 MyPromise,没有处理 "等待它完成" 的逻辑。
完整版需要实现 resolvePromise 函数(面试常考):
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('循环引用'));
}
if (x instanceof MyPromise) {
// 如果 x 是 Promise,等待它的结果
x.then(
(value) => resolve(value),
(err) => reject(err)
);
} else {
// 普通值直接 resolve
resolve(x);
}
}
然后在你 then 里的 resolve(x) 改成 resolvePromise(promise2, x, resolve, reject)。
十,异步迭代器与 for-await-of
当你处理流式数据(比如分页请求、WebSocket 消息流、逐行读取大文件)时,普通 for...of 不够用,需要 for-await-of。
10.1 同步迭代器 vs 异步迭代器
| 类型 | 协议 | 方法 | 使用方式 |
|---|---|---|---|
| 同步迭代器 | Iterable | next() 返回 { value, done } | for...of |
| 异步迭代器 | AsyncIterable | next() 返回 Promise<{ value, done }> | for-await-of |
10.2 异步生成器
// 定义一个异步生成器
async function* asyncNumberGenerator() {
for (let i = 1; i <= 3; i++) {
await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟异步
yield i; // 每秒吐出一个数字
}
}
// 使用 for-await-of 消费
(async () => {
console.log('开始');
for await (const num of asyncNumberGenerator()) {
console.log('收到:', num);
}
console.log('结束');
})();
// 输出(每隔 1 秒输出一行):
// 开始
// 收到: 1
// 收到: 2
// 收到: 3
// 结束
10.3 实际场景
// 模拟分页 API:每页 2 条
async function fetchPage(page) {
const data = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 3, name: 'C' },
{ id: 4, name: 'D' },
{ id: 5, name: 'E' },
];
const start = (page - 1) * 2;
const end = start + 2;
await new Promise(r => setTimeout(r, 500)); // 模拟网络延迟
return {
items: data.slice(start, end),
hasMore: end < data.length,
};
}
// 异步生成器:逐页获取,直到没有更多数据
async function* paginate() {
let page = 1;
let hasMore = true;
while (hasMore) {
const result = await fetchPage(page);
yield* result.items; // yield* 逐个吐出 items 中的元素
hasMore = result.hasMore;
page++;
}
}
// 消费:逐条处理
(async () => {
for await (const item of paginate()) {
console.log('处理用户:', item.name);
// 可以在这里逐个处理,不需要等所有数据加载完
}
})();
// 输出:
// 处理用户: A
// 处理用户: B (等待 500ms)
// 处理用户: C
// 处理用户: D (等待 500ms)
// 处理用户: E
10.4 对比
不用迭代器(一次性加载所有数据):
const allItems = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const result = await fetchPage(page);
allItems.push(...result.items);
hasMore = result.hasMore;
page++;
}
// 所有数据加载完才能开始处理
allItems.forEach(item => console.log(item));
// 缺点:内存占用大,处理延迟高
用异步迭代器(边加载边处理):
for await (const item of paginate()) {
console.log(item); // 来一条处理一条
}
// 优点:内存友好,响应快
10.5 把普通异步函数变成可迭代
// 将事件流(如 WebSocket)包装成异步迭代器
function createAsyncIterable(ws) {
return {
[Symbol.asyncIterator]() {
return {
next() {
return new Promise((resolve) => {
ws.onmessage = (event) => {
resolve({ value: event.data, done: false });
};
ws.onclose = () => {
resolve({ value: undefined, done: true });
};
});
}
};
}
};
}
// 使用
const ws = new WebSocket('wss://example.com');
for await (const message of createAsyncIterable(ws)) {
console.log('收到消息:', message);
}
问:for-await-of 和 for...of 有什么区别?
| 维度 | for...of | for-await-of |
|---|---|---|
| 适用对象 | 同步可迭代对象 | 异步可迭代对象 |
| next() 返回值 | 直接返回 { value, done } | 返回 Promise<{ value, done }> |
| 使用时 | 任何地方直接使用 | 必须在 async 函数内 |
| 底层 | 同步执行 | 每次迭代都会 await |
10.6 什么时候用异步迭代器?
| 场景 | 传统方案 | 异步迭代器方案 |
|---|---|---|
| 分页加载大量数据 | while + await,一次性 push 到数组 | for-await-of 逐条处理 |
| WebSocket 消息流 | 用 onmessage 回调 | 包装成迭代器,更优雅 |
| 大文件逐行读取 | 回调或 stream 事件 | for-await-of 逐行处理 |
| 定时轮询 | setInterval | 异步生成器 + for-await-of |