异步编程
问题
JavaScript 异步编程有哪些方式?Promise 和 async/await 如何使用?
答案
JavaScript 是单线程语言,使用异步编程来处理耗时操作,避免阻塞主线程。异步编程经历了从回调函数到 Promise 再到 async/await 的演进。
异步编程演进
回调函数
// 回调地狱示例
function getUserData(userId: string, callback: (user: any) => void): void {
setTimeout(() => {
callback({ id: userId, name: 'Alice' });
}, 100);
}
function getOrders(userId: string, callback: (orders: any[]) => void): void {
setTimeout(() => {
callback([{ id: 1, product: 'Phone' }]);
}, 100);
}
function getOrderDetails(orderId: number, callback: (details: any) => void): void {
setTimeout(() => {
callback({ id: orderId, price: 999 });
}, 100);
}
// ❌ 回调地狱 - 难以阅读和维护
getUserData('1', (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
console.log(details);
// 更多嵌套...
});
});
});
Promise
基本用法
// 创建 Promise
const promise = new Promise<string>((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve('成功');
} else {
reject(new Error('失败'));
}
}, 100);
});
// 使用 Promise
promise
.then((value) => console.log(value))
.catch((error) => console.error(error))
.finally(() => console.log('完成'));
Promise 状态
链式调用
function fetchUser(id: string): Promise<{ id: string; name: string }> {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, name: 'Alice' }), 100);
});
}
function fetchOrders(userId: string): Promise<{ id: number; product: string }[]> {
return new Promise((resolve) => {
setTimeout(() => resolve([{ id: 1, product: 'Phone' }]), 100);
});
}
// ✅ 链式调用 - 扁平化
fetchUser('1')
.then((user) => {
console.log('用户:', user);
return fetchOrders(user.id);
})
.then((orders) => {
console.log('订单:', orders);
return orders[0];
})
.then((order) => {
console.log('第一个订单:', order);
})
.catch((error) => {
console.error('错误:', error);
});
Promise 静态方法
// Promise.all - 全部成功才成功
const promises = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3),
];
Promise.all(promises)
.then((values) => console.log(values)) // [1, 2, 3]
.catch((error) => console.error(error)); // 任一失败则失败
// Promise.allSettled - 等待全部完成(不管成功失败)
Promise.allSettled([
Promise.resolve(1),
Promise.reject('error'),
Promise.resolve(3),
]).then((results) => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 'error' },
// { status: 'fulfilled', value: 3 }
// ]
});
// Promise.race - 第一个完成的结果
Promise.race([
new Promise(resolve => setTimeout(() => resolve('slow'), 200)),
new Promise(resolve => setTimeout(() => resolve('fast'), 100)),
]).then((value) => console.log(value)); // 'fast'
// Promise.any - 第一个成功的结果
Promise.any([
Promise.reject('error1'),
new Promise(resolve => setTimeout(() => resolve('success'), 100)),
Promise.reject('error2'),
]).then((value) => console.log(value)); // 'success'
错误处理
// 方式 1: catch 方法
fetchData()
.then(processData)
.catch((error) => {
console.error('处理错误:', error);
});
// 方式 2: then 的第二个参数
fetchData()
.then(
(data) => processData(data),
(error) => console.error(error)
);
// 建议使用 catch - 可以捕获整个链的错误
fetchData()
.then(processData)
.then(saveData)
.catch((error) => {
// 可以捕获 fetchData、processData、saveData 任一环节的错误
console.error(error);
});
async/await
基本用法
async function fetchUserData(userId: string): Promise<{ user: any; orders: any[] }> {
try {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
return { user, orders };
} catch (error) {
console.error('获取数据失败:', error);
throw error;
}
}
// 调用
const data = await fetchUserData('1');
console.log(data);
async/await 特点
// 1. async 函数返回 Promise
async function foo(): Promise<number> {
return 1;
}
// 等价于
function foo(): Promise<number> {
return Promise.resolve(1);
}
// 2. await 暂停执行,等待 Promise 解决
async function bar(): Promise<void> {
console.log(1);
await Promise.resolve();
console.log(2); // 微任务
}
bar();
console.log(3);
// 输出: 1, 3, 2
// 3. await 可以处理非 Promise 值
async function baz(): Promise<number> {
const value = await 42; // 自动包装为 Promise.resolve(42)
return value;
}
并行执行
// ❌ 串行执行 - 慢
async function serial(): Promise<void> {
const user = await fetchUser('1'); // 100ms
const orders = await fetchOrders('1'); // 100ms
// 总共 200ms
}
// ✅ 并行执行 - 快
async function parallel(): Promise<void> {
const [user, orders] = await Promise.all([
fetchUser('1'),
fetchOrders('1'),
]);
// 总共 100ms
}
// 部分并行,部分串行
async function mixed(): Promise<void> {
// 先获取用户
const user = await fetchUser('1');
// 然后并行获取订单和收藏
const [orders, favorites] = await Promise.all([
fetchOrders(user.id),
fetchFavorites(user.id),
]);
}
错误处理
// try/catch
async function withTryCatch(): Promise<void> {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error('错误:', error);
}
}
// 统一错误处理函数
async function to<T>(
promise: Promise<T>
): Promise<[Error | null, T | null]> {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error as Error, null];
}
}
// 使用
async function example(): Promise<void> {
const [error, data] = await to(fetchData());
if (error) {
console.error('错误:', error);
return;
}
console.log('数据:', data);
}
循环中的 async/await
const ids = ['1', '2', '3'];
// ❌ forEach 不会等待
ids.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user);
});
console.log('完成'); // 先输出,再输出用户
// ✅ for...of 会等待
async function sequential(): Promise<void> {
for (const id of ids) {
const user = await fetchUser(id);
console.log(user);
}
console.log('完成'); // 最后输出
}
// ✅ 并行处理
async function parallelProcess(): Promise<void> {
const users = await Promise.all(ids.map((id) => fetchUser(id)));
users.forEach((user) => console.log(user));
console.log('完成');
}
Generator 与异步
什么是 Generator
Generator(生成器)是 ES6 引入的一种可以暂停和恢复的函数。普通函数一旦执行就会一口气跑完,而 Generator 函数可以在执行过程中通过 yield 关键字暂停,之后再通过 next() 方法恢复执行。
用 function* 声明一个 Generator 函数,调用它不会立即执行函数体,而是返回一个迭代器对象(Generator 对象),通过调用迭代器的 next() 方法来逐步执行。
基本用法
function* simpleGenerator(): Generator<number, string, unknown> {
console.log('第一段代码');
yield 1; // 暂停,把 1 交给外部
console.log('第二段代码');
yield 2; // 再次暂停,把 2 交给外部
console.log('第三段代码');
return '结束'; // 函数结束
}
// 调用 Generator 函数,得到迭代器(此时函数体还没执行!)
const gen = simpleGenerator();
// 第一次 next():执行到第一个 yield,暂停
gen.next(); // 打印 "第一段代码",返回 { value: 1, done: false }
// 第二次 next():从上次暂停处继续,执行到第二个 yield
gen.next(); // 打印 "第二段代码",返回 { value: 2, done: false }
// 第三次 next():从上次暂停处继续,执行到 return
gen.next(); // 打印 "第三段代码",返回 { value: '结束', done: true }
每次调用 next() 都返回一个对象 { value, done }:
value:yield后面的值(或return的值)done:false表示还没结束,true表示函数已执行完毕
next() 传值:外部向 Generator 内部传递数据
next() 方法可以接收一个参数,这个参数会作为上一个 yield 表达式的返回值。这是 Generator 最关键的特性——双向通信。
function* adder(): Generator<number, string, number> {
const a = yield 1; // 第一次暂停。a 的值不是 1,而是下次 next() 传入的值
console.log('a =', a);
const b = yield 2; // 第二次暂停。b 的值是下次 next() 传入的值
console.log('b =', b);
return `结果: ${a + b}`;
}
const gen = adder();
// 第一次 next():执行到 yield 1,返回 { value: 1, done: false }
// ⚠️ 第一次 next() 的参数会被忽略(因为还没有"上一个 yield")
gen.next();
// 第二次 next(10):从 yield 1 处恢复,10 作为 yield 1 的返回值赋给 a
// 继续执行到 yield 2,返回 { value: 2, done: false }
gen.next(10); // 打印 "a = 10"
// 第三次 next(20):从 yield 2 处恢复,20 作为 yield 2 的返回值赋给 b
// 执行到 return,返回 { value: '结果: 30', done: true }
gen.next(20); // 打印 "b = 20"
yield 既是输出也是输入:
- 输出:
yield后面的值(如yield 1中的1)通过next()的返回值交给外部 - 输入:下一次
next(value)传入的value作为整个yield表达式的返回值
简单记忆:yield 送出去的值和 yield 接收到的值是两回事,它们通过两次不同的 next() 调用分别交换。
下面用一张时序图来帮助理解执行过程:
Generator 实现异步(async/await 的前身)
理解了 yield 的暂停/恢复和 next() 传值后,我们就能理解如何用 Generator 实现异步编程了。核心思路:
yield一个 Promise 出去(暂停函数执行)- 外部等 Promise resolve 后,通过
next(result)把结果传回来(恢复函数执行) - Generator 内部拿到结果继续执行,遇到下一个
yield再暂停
function* fetchFlow(): Generator<Promise<any>, void, any> {
// yield 一个 Promise 出去,暂停
// 等 Promise resolve 后,外部把结果通过 next() 传回来,赋值给 user
const user = yield fetchUser('1');
console.log('用户:', user);
// 同理,yield 另一个 Promise
const orders = yield fetchOrders(user.id);
console.log('订单:', orders);
}
但 Generator 不会自动执行,需要我们写一个执行器来驱动它——不断调用 next(),等 Promise 完成后传结果回去:
function co(generatorFn: () => Generator<Promise<any>, any, any>): Promise<any> {
return new Promise((resolve, reject) => {
const gen = generatorFn();
function step(nextValue?: any): void {
let result;
try {
result = gen.next(nextValue); // 传值并恢复执行
} catch (e) {
reject(e);
return;
}
if (result.done) {
resolve(result.value); // Generator 执行完毕
return;
}
// result.value 是一个 Promise,等它 resolve 后继续
Promise.resolve(result.value)
.then(step) // 成功:把结果传给下一个 next()
.catch((e) => gen.throw(e)); // 失败:向 Generator 内部抛异常
}
step(); // 启动
});
}
// 使用执行器驱动 Generator
co(fetchFlow).then(() => console.log('全部完成'));
理解了 Generator + 执行器后,你会发现 async/await 就是这个模式的语法糖:
async function=function*+ 内置自动执行器await=yield- 引擎帮你自动完成了
co执行器的工作,不需要手动写
// 这两种写法在行为上是等价的:
async function fetchData() {
const user = await fetchUser('1');
const orders = await fetchOrders(user.id);
}
co(function* () {
const user = yield fetchUser('1');
const orders = yield fetchOrders(user.id);
});
异步方案对比
| 特性 | 回调 | Promise | async/await |
|---|---|---|---|
| 代码风格 | 嵌套 | 链式 | 同步 |
| 错误处理 | 每层处理 | catch | try/catch |
| 调试 | 困难 | 一般 | 简单 |
| 取消 | 困难 | 需封装 | 需封装 |
| 并发控制 | 手动 | Promise.all | Promise.all |
实际应用
请求重试
async function fetchWithRetry<T>(
fn: () => Promise<T>,
retries: number = 3,
delay: number = 1000
): Promise<T> {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
console.log(`重试 ${i + 1}/${retries}`);
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
}
}
throw new Error('Unreachable');
}
// 使用
const data = await fetchWithRetry(() => fetch('/api/data').then(r => r.json()));
并发限制
async function asyncPool<T, R>(
limit: number,
items: T[],
fn: (item: T) => Promise<R>
): Promise<R[]> {
const results: R[] = [];
const executing: Promise<void>[] = [];
for (const item of items) {
const p = fn(item).then((result) => {
results.push(result);
});
executing.push(p as unknown as Promise<void>);
if (executing.length >= limit) {
await Promise.race(executing);
executing.splice(
executing.findIndex((p2) => p2 === p),
1
);
}
}
await Promise.all(executing);
return results;
}
// 使用:最多同时 3 个请求
const urls = ['url1', 'url2', 'url3', 'url4', 'url5'];
const results = await asyncPool(3, urls, fetchUrl);
超时处理
function withTimeout<T>(
promise: Promise<T>,
timeout: number
): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Timeout after ${timeout}ms`));
}, timeout);
}),
]);
}
// 使用
try {
const data = await withTimeout(fetchData(), 5000);
} catch (error) {
console.error('请求超时或失败');
}
常见面试问题
Q1: Promise 的三种状态是什么?
答案:
| 状态 | 说明 | 转换 |
|---|---|---|
pending | 等待中 | 初始状态 |
fulfilled | 已完成 | resolve() 触发 |
rejected | 已拒绝 | reject() 触发 |
注意:状态一旦改变就不可逆。
Q2: Promise.all 和 Promise.allSettled 的区别?
答案:
| 方法 | 成功条件 | 失败条件 | 返回值 |
|---|---|---|---|
all | 全部成功 | 任一失败 | 结果数组 |
allSettled | 无 | 无 | 状态对象数组 |
// all - 任一失败立即失败
Promise.all([p1, p2, p3])
.then(([r1, r2, r3]) => { })
.catch((error) => { });
// allSettled - 等待全部完成
Promise.allSettled([p1, p2, p3])
.then((results) => {
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.log('失败:', result.reason);
}
});
});
Q3: async/await 的原理是什么?
答案:
async/await 是 Generator + Promise 的语法糖:
// async/await
async function foo() {
const a = await Promise.resolve(1);
const b = await Promise.resolve(2);
return a + b;
}
// 等价于 Generator
function* fooGenerator() {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(2);
return a + b;
}
// + 自动执行器
Q4: 如何实现异步任务的取消?
答案:
// 使用 AbortController
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then((response) => response.json())
.catch((error) => {
if (error.name === 'AbortError') {
console.log('请求已取消');
}
});
// 取消请求
controller.abort();
Q5: 以下代码输出什么?
async function async1(): Promise<void> {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2(): Promise<void> {
console.log('async2');
}
console.log('script start');
async1();
console.log('script end');
答案:
script start
async1 start
async2
script end
async1 end
分析:
- 同步代码:
script start - 调用 async1:
async1 start - 调用 async2:
async2 - await 后的代码放入微任务队列
- 同步代码:
script end - 执行微任务:
async1 end
Q6: Promise.all、Promise.allSettled、Promise.race、Promise.any 的区别和使用场景?
答案:
四个静态方法都接收一个 Promise 可迭代对象,但行为和返回值各不相同:
| 方法 | 成功条件 | 失败条件 | 短路行为 | 返回值 |
|---|---|---|---|---|
Promise.all | 全部 fulfilled | 任一 rejected | 遇到 reject 立即短路 | T[] 结果数组 |
Promise.allSettled | 无(始终 fulfilled) | 无 | 不短路,等所有完成 | PromiseSettledResult<T>[] |
Promise.race | 第一个 settled | 第一个 settled | 第一个完成就短路 | 第一个结果(成功或失败) |
Promise.any | 任一 fulfilled | 全部 rejected | 遇到第一个成功就短路 | 第一个成功的值 |
// 模拟请求
const fast = (): Promise<string> =>
new Promise((resolve) => setTimeout(() => resolve('fast'), 100));
const slow = (): Promise<string> =>
new Promise((resolve) => setTimeout(() => resolve('slow'), 300));
const fail = (): Promise<string> =>
new Promise((_, reject) => setTimeout(() => reject(new Error('fail')), 200));
// ✅ Promise.all - 全部成功才返回,适合「必须全部成功」的场景
async function demoAll(): Promise<void> {
try {
const results = await Promise.all([fast(), slow()]);
console.log(results); // ['fast', 'slow']
} catch (error) {
// 任一失败就进入 catch
}
}
// ✅ Promise.allSettled - 无论成功失败都返回,适合「批量操作容忍部分失败」
async function demoAllSettled(): Promise<void> {
const results = await Promise.allSettled([fast(), fail(), slow()]);
// results 格式:
// [
// { status: 'fulfilled', value: 'fast' },
// { status: 'rejected', reason: Error('fail') },
// { status: 'fulfilled', value: 'slow' }
// ]
const succeeded = results.filter(
(r): r is PromiseFulfilledResult<string> => r.status === 'fulfilled'
);
const failed = results.filter(
(r): r is PromiseRejectedResult => r.status === 'rejected'
);
console.log(`成功 ${succeeded.length} 个,失败 ${failed.length} 个`);
}
// ✅ Promise.race - 最快的决定结果,适合「超时控制」
async function demoRace(): Promise<void> {
const timeout = (): Promise<never> =>
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 150)
);
try {
const result = await Promise.race([slow(), timeout()]);
console.log(result);
} catch (error) {
console.error('请求超时'); // slow 需要 300ms,超时 150ms,因此超时
}
}
// ✅ Promise.any - 第一个成功的,适合「多源竞速取最快可用」
async function demoAny(): Promise<void> {
try {
const result = await Promise.any([fail(), fast(), slow()]);
console.log(result); // 'fast'(忽略 fail,取第一个成功的)
} catch (error) {
// 全部失败才进入 catch,error 是 AggregateError
if (error instanceof AggregateError) {
console.error('全部失败:', error.errors);
}
}
}
- Promise.all:并行请求多个接口,全部成功后渲染页面(如同时获取用户信息 + 订单列表 + 配置数据)
- Promise.allSettled:批量操作允许部分失败(如批量删除、批量上传,需要知道每个的成功/失败状态)
- Promise.race:超时控制、取最快的 CDN 节点
- Promise.any:多个镜像源选最快的、多个 API 备用地址容灾
Q7: async/await 的错误处理最佳实践?
答案:
async/await 的错误处理有多种方式,每种适合不同场景:
方式一:try/catch(最常用)
async function fetchUserData(userId: string): Promise<User> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
// 区分错误类型
if (error instanceof TypeError) {
console.error('网络错误:', error.message);
} else if (error instanceof SyntaxError) {
console.error('JSON 解析失败:', error.message);
} else {
console.error('请求失败:', error);
}
throw error; // 重新抛出,让调用方决定如何处理
}
}
方式二:to 函数包装(Go 风格,消除 try/catch 嵌套)
type Result<T> = [null, T] | [Error, null];
async function to<T>(promise: Promise<T>): Promise<Result<T>> {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error instanceof Error ? error : new Error(String(error)), null];
}
}
// 使用:告别嵌套的 try/catch
async function handleUserAction(userId: string): Promise<void> {
const [err1, user] = await to(fetchUser(userId));
if (err1) {
showToast('获取用户失败');
return;
}
const [err2, orders] = await to(fetchOrders(user!.id));
if (err2) {
showToast('获取订单失败');
return;
}
console.log('用户和订单:', user, orders);
}
方式三:.catch() 链式处理(适合单个 await 的错误处理)
async function loadConfig(): Promise<Config> {
const config = await fetchConfig().catch(() => DEFAULT_CONFIG);
// config 一定有值,不需要 try/catch
return config;
}
// 适合需要给默认值的场景
async function loadPage(): Promise<void> {
const [user, settings] = await Promise.all([
fetchUser('1').catch(() => null), // 失败则返回 null
fetchSettings().catch(() => defaults), // 失败则用默认值
]);
renderPage(user, settings);
}
方式四:全局错误处理
// 浏览器
window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
console.error('未处理的 Promise 拒绝:', event.reason);
event.preventDefault(); // 阻止控制台报错
// 上报错误监控
reportError(event.reason);
});
// Node.js
process.on('unhandledRejection', (reason: unknown, promise: Promise<unknown>) => {
console.error('未处理的 Promise 拒绝:', reason);
reportError(reason);
});
- 不要吞掉错误:catch 中至少要记录日志或上报,不能写空的 catch 块
- 不要在 forEach 中使用 async/await:forEach 不会等待异步完成,用
for...of或Promise.all + map代替 - 注意 await 位置:
return await promise和return promise在 try/catch 中行为不同 -- 前者能被 catch 捕获,后者不能
// ❌ 错误不会被 catch 捕获
async function bad(): Promise<string> {
try {
return fetchData(); // 直接返回 Promise,catch 捕获不到 fetchData 的错误
} catch (error) {
console.error(error); // 永远不会执行
return 'fallback';
}
}
// ✅ 错误能被 catch 捕获
async function good(): Promise<string> {
try {
return await fetchData(); // await 让错误能被 catch 捕获
} catch (error) {
console.error(error);
return 'fallback';
}
}
Q8: 如何实现一个可取消的 Promise(AbortController)?
答案:
JavaScript 原生 Promise 不支持取消,但可以通过 AbortController 实现取消功能。AbortController 是 Web 标准 API,Node.js 15+ 也原生支持。
基础用法:取消 fetch 请求
const controller = new AbortController();
const { signal } = controller;
fetch('/api/data', { signal })
.then((res) => res.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === 'AbortError') {
console.log('请求被取消');
} else {
console.error('请求失败:', error);
}
});
// 5 秒后取消
setTimeout(() => controller.abort(), 5000);
封装通用的可取消 Promise
interface CancelablePromise<T> {
promise: Promise<T>;
cancel: (reason?: string) => void;
}
function makeCancelable<T>(
executor: (signal: AbortSignal) => Promise<T>
): CancelablePromise<T> {
const controller = new AbortController();
const promise = new Promise<T>((resolve, reject) => {
// 如果已经取消,直接拒绝
if (controller.signal.aborted) {
reject(new DOMException('Canceled', 'AbortError'));
return;
}
// 监听取消事件
controller.signal.addEventListener('abort', () => {
reject(new DOMException(
controller.signal.reason || 'Canceled',
'AbortError'
));
});
// 执行实际逻辑
executor(controller.signal).then(resolve, reject);
});
return {
promise,
cancel: (reason?: string) => controller.abort(reason),
};
}
// 使用示例
const { promise, cancel } = makeCancelable(async (signal) => {
const res = await fetch('/api/slow-data', { signal });
return res.json();
});
promise
.then((data) => console.log('数据:', data))
.catch((err) => {
if (err.name === 'AbortError') console.log('已取消');
});
// 需要取消时
cancel('用户离开页面');
实战:React 中用 AbortController 防止内存泄漏
import { useState, useEffect } from 'react';
function useFetch<T>(url: string): { data: T | null; loading: boolean; error: Error | null } {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((json: T) => {
setData(json);
setError(null);
})
.catch((err) => {
if (err.name !== 'AbortError') {
setError(err);
}
})
.finally(() => setLoading(false));
return () => controller.abort(); // 组件卸载时取消请求
}, [url]);
return { data, loading, error };
}
AbortController 配合超时
async function fetchWithTimeout<T>(
url: string,
options: RequestInit = {},
timeout: number = 5000
): Promise<T> {
const controller = new AbortController();
// AbortSignal.timeout() 是更简洁的写法(较新 API)
// 兼容写法:手动设置定时器
const timeoutId = setTimeout(() => controller.abort('Timeout'), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} finally {
clearTimeout(timeoutId);
}
}
// 使用
try {
const data = await fetchWithTimeout<User>('/api/user', {}, 3000);
console.log(data);
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
console.error('请求超时');
}
}
ES2024+ 新增了更便捷的 API:
AbortSignal.timeout(ms):创建一个超时后自动取消的 signalAbortSignal.any([signal1, signal2]):任一 signal 取消时触发,类似 Promise.race
// 同时支持手动取消和超时取消
const controller = new AbortController();
const signal = AbortSignal.any([
controller.signal, // 手动取消
AbortSignal.timeout(5000), // 5 秒超时
]);
fetch('/api/data', { signal });
Q9: Promise 并发控制(限制同时执行的 Promise 数量)?
答案:
在批量请求场景下(如批量上传文件、爬虫),如果不做并发限制,可能导致服务端压力过大或浏览器连接数耗尽。核心思路是维护一个执行池,池满时等待某个完成后再放入新任务。
方式一:基于 Promise.race 的 asyncPool
async function asyncPool<T, R>(
limit: number,
items: T[],
iteratorFn: (item: T) => Promise<R>
): Promise<R[]> {
const results: R[] = [];
const executing: Set<Promise<void>> = new Set();
for (const [index, item] of items.entries()) {
// 创建任务 Promise
const p = iteratorFn(item).then((result) => {
results[index] = result; // 保持顺序
});
// 任务完成后从执行池移除
const clean = p.then(() => executing.delete(clean));
executing.add(clean);
// 达到并发上限,等待最快完成的那个
if (executing.size >= limit) {
await Promise.race(executing);
}
}
// 等待剩余任务完成
await Promise.all(executing);
return results;
}
// 使用示例
const urls = Array.from({ length: 20 }, (_, i) => `/api/item/${i}`);
const results = await asyncPool(3, urls, async (url) => {
const res = await fetch(url);
return res.json();
});
console.log(results); // 按原顺序返回 20 个结果
方式二:基于类的调度器(Scheduler)
class Scheduler {
private maxConcurrent: number;
private running: number = 0;
private queue: Array<() => void> = [];
constructor(maxConcurrent: number) {
this.maxConcurrent = maxConcurrent;
}
add<T>(task: () => Promise<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
const run = async (): Promise<void> => {
this.running++;
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.next();
}
};
if (this.running < this.maxConcurrent) {
run();
} else {
this.queue.push(run);
}
});
}
private next(): void {
if (this.queue.length > 0 && this.running < this.maxConcurrent) {
const nextTask = this.queue.shift()!;
nextTask();
}
}
}
// 使用示例
const scheduler = new Scheduler(2); // 最多同时 2 个
const delay = (ms: number, label: string): Promise<string> =>
new Promise((resolve) => {
console.log(`${label} 开始`);
setTimeout(() => {
console.log(`${label} 完成`);
resolve(label);
}, ms);
});
scheduler.add(() => delay(1000, 'A')); // 立即执行
scheduler.add(() => delay(500, 'B')); // 立即执行
scheduler.add(() => delay(300, 'C')); // 等待,B 完成后执行
scheduler.add(() => delay(400, 'D')); // 等待,C 完成后执行
// 输出顺序: A开始 → B开始 → B完成 → C开始 → C完成 → D开始 → A完成 → D完成
方式三:用 for await + 信号量控制
class Semaphore {
private permits: number;
private waitQueue: Array<() => void> = [];
constructor(permits: number) {
this.permits = permits;
}
async acquire(): Promise<void> {
if (this.permits > 0) {
this.permits--;
return;
}
return new Promise<void>((resolve) => {
this.waitQueue.push(resolve);
});
}
release(): void {
this.permits++;
if (this.waitQueue.length > 0) {
this.permits--;
const next = this.waitQueue.shift()!;
next();
}
}
}
// 使用示例
async function batchFetch(urls: string[], limit: number): Promise<Response[]> {
const semaphore = new Semaphore(limit);
const tasks = urls.map(async (url) => {
await semaphore.acquire();
try {
return await fetch(url);
} finally {
semaphore.release();
}
});
return Promise.all(tasks);
}
| 方式 | 优点 | 适合场景 |
|---|---|---|
| asyncPool | 简洁,结果有序 | 批量处理固定数组 |
| Scheduler | 动态添加任务,可扩展 | 运行时不断有新任务加入 |
| Semaphore | 通用,可组合 | 需要在任意位置控制并发 |
Q10: Generator 函数的原理,以及它和 async/await 的关系?
答案:
Generator 函数是 ES6 引入的特殊函数,通过 function* 声明,内部通过 yield 关键字暂停和恢复执行。它是理解 async/await 底层原理的关键 -- async/await 本质上就是 Generator + 自动执行器的语法糖。
Generator 基本原理
function* counter(): Generator<number, string, boolean> {
console.log('开始');
const reset = yield 1; // 第一次暂停,返回 1
console.log('reset =', reset);
yield 2; // 第二次暂停,返回 2
return '结束'; // 完成
}
const gen = counter();
// 调用 next() 驱动执行
console.log(gen.next()); // 开始 → { value: 1, done: false }
console.log(gen.next(false)); // reset = false → { value: 2, done: false }
console.log(gen.next()); // { value: '结束', done: true }
Generator 实现异步控制
Generator 的 yield 可以暂停函数,如果 yield 的是 Promise,外部等 Promise resolve 后再调用 next(result) 把结果传回去,就实现了"用同步写法写异步":
function* fetchFlow(): Generator<Promise<any>, void, any> {
const user = yield fetch('/api/user').then((r) => r.json());
console.log('用户:', user);
const orders = yield fetch(`/api/orders/${user.id}`).then((r) => r.json());
console.log('订单:', orders);
}
但 Generator 不会自动执行,需要一个执行器来驱动:
function co<T>(generatorFn: () => Generator<Promise<any>, T, any>): Promise<T> {
return new Promise((resolve, reject) => {
const gen = generatorFn();
function step(nextFn: () => IteratorResult<Promise<any>, T>): void {
let result: IteratorResult<Promise<any>, T>;
try {
result = nextFn();
} catch (error) {
reject(error);
return;
}
if (result.done) {
resolve(result.value);
return;
}
// yield 的值是 Promise,等它 resolve 后继续
Promise.resolve(result.value).then(
(value) => step(() => gen.next(value)), // 成功:传值回去
(error) => step(() => gen.throw(error)) // 失败:抛异常
);
}
step(() => gen.next());
});
}
// 使用
co(fetchFlow).then(() => console.log('全部完成'));
Generator 与 async/await 的对应关系
// ✅ async/await 写法
async function fetchData(): Promise<void> {
try {
const user = await fetch('/api/user').then((r) => r.json());
const orders = await fetch(`/api/orders/${user.id}`).then((r) => r.json());
console.log(user, orders);
} catch (error) {
console.error(error);
}
}
// ✅ 等价的 Generator + co 写法
co(function* (): Generator<Promise<any>, void, any> {
try {
const user = yield fetch('/api/user').then((r) => r.json());
const orders = yield fetch(`/api/orders/${user.id}`).then((r) => r.json());
console.log(user, orders);
} catch (error) {
console.error(error);
}
});
| 对比项 | Generator | async/await |
|---|---|---|
| 声明 | function* | async function |
| 暂停关键字 | yield | await |
| 执行方式 | 需要手动调用 next() 或执行器(co) | 引擎自动执行 |
| 返回值 | Generator 迭代器对象 | Promise |
| 错误处理 | gen.throw(error) | try/catch |
| 语义 | 通用的暂停/恢复(迭代器协议) | 专门用于异步 |
| 双向通信 | yield 可传值出去,next() 可传值进来 | await 只获取结果 |
- async/await 是 Generator 的语法糖:
async对应function*,await对应yield,引擎内置了自动执行器(类似 co 函数) - Generator 比 async/await 更通用:Generator 实现了迭代器协议,可用于惰性序列、状态机等非异步场景;async/await 只用于异步
- Babel 转译 async/await 时,早期版本就是将其转为 Generator + 执行器的形式(可参考 regenerator-runtime)
补充:Generator 的其他应用场景
Generator 不仅用于异步,还有以下经典用途:
function* fibonacci(): Generator<number, never, unknown> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// 按需取值,不会一次性计算所有值
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
function* trafficLight(): Generator<string, never, unknown> {
while (true) {
yield 'green';
yield 'yellow';
yield 'red';
}
}
const light = trafficLight();
console.log(light.next().value); // 'green'
console.log(light.next().value); // 'yellow'
console.log(light.next().value); // 'red'
console.log(light.next().value); // 'green'(循环)