JavaScript 异步编程模式
JavaScript 异步编程模式
JavaScript 是一门单线程语言,异步编程是其核心特性之一,掌握各种异步编程模式对于编写高性能的应用至关重要。本文将从最基础的回调函数出发,逐步深入到 Promise、async/await、事件循环、微任务与宏任务、生成器、异步迭代器、并发控制等主题,并结合大量真实案例、性能对比数据与常见踩坑,帮助你建立完整的异步编程知识体系。
为什么 JavaScript 需要异步
单线程的本质:
JavaScript 最初被设计为在浏览器中运行的脚本语言,用来操作 DOM、响应用户交互。如果 JavaScript 是多线程的,那么当一个线程修改 DOM 而另一个线程删除同一个 DOM 节点时,浏览器就无法确定应该以哪个操作为准。为了避免这种复杂的同步问题,JavaScript 被设计成单线程。
单线程带来的问题:
单线程意味着同一时刻只能做一件事。如果某个操作非常耗时(例如网络请求、读取大文件、复杂计算),那么在这个操作完成之前,整个线程都会被阻塞,页面无法响应用户的任何操作,表现为"卡死"。
类比:餐厅只有一个服务员
想象一家餐厅只有一位服务员(单线程)。如果这位服务员在为一桌客人点餐后,必须站在厨房门口一直等到这道菜做好、端上桌,才能去招待下一桌客人,那么效率会极其低下——后面的客人只能干等。
聪明的做法是:服务员记下这桌客人的订单(发起异步任务),把订单交给厨房(交给浏览器的其他线程/内核),然后立刻去招待下一桌客人。当某道菜做好时,厨房会通知服务员(回调/事件),服务员再把菜端过去。这样一位服务员就能同时"照顾"很多桌客人。这就是 JavaScript 异步编程的核心思想:遇到耗时操作时不要傻等,先去做别的事,等结果好了再回来处理。
同步 vs 异步:
// 同步代码:一行执行完才执行下一行
console.log('第一步');
const result = doHeavyCalculation(); // 假设耗时 3 秒,这 3 秒页面完全卡死
console.log('第二步:' + result);
// 异步代码:耗时操作交给后台,主线程继续往下走
console.log('第一步');
setTimeout(() => {
console.log('第三步:延迟任务完成'); // 1 秒后才执行
}, 1000);
console.log('第二步'); // 立即执行,不会被上面的 setTimeout 阻塞
// 输出顺序:
// 第一步
// 第二步
// 第三步:延迟任务完成异步的重要性:
回调函数
回调函数的概念:
为什么回调重要:
回调是理解一切异步模式的基础。Promise、async/await 本质上都是对回调模式的封装与改进。理解回调的优点和缺陷,才能明白后续方案到底解决了什么问题。
回调的分类:
Node.js 的错误优先回调约定(error-first callback):
Node.js 生态约定回调的第一个参数永远是错误对象,如果没有错误则为 null,后续参数才是数据。这是为了统一错误处理。
回调地狱:
代码示例
同步回调与异步回调
// 同步回调:函数在当前调用栈内立即执行
[1, 2, 3].forEach(function (item) {
console.log('同步回调:' + item);
});
console.log('forEach 之后');
// 输出顺序:同步回调 1 / 2 / 3 / forEach 之后
// 异步回调:函数被推迟到未来的事件循环执行
console.log('注册定时器之前');
setTimeout(function () {
console.log('异步回调:定时器触发');
}, 0);
console.log('注册定时器之后');
// 输出顺序:注册定时器之前 / 注册定时器之后 / 异步回调基本回调函数
// 基本回调函数(错误优先约定)
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'Alice', age: 25 };
callback(null, data);
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error('Error:', error);
return;
}
console.log('Data:', data);
});回调地狱
function fetchData(cb) {
setTimeout(() => cb(null, { id: 1, name: 'Alice' }), 1000);
}
function processData(data, cb) {
setTimeout(() => cb(null, { ...data, processed: true }), 500);
}
function saveData(data, cb) {
setTimeout(() => cb(null, { success: true, data }), 300);
}
// 回调地狱:层层嵌套,向右缩进越来越深
fetchData((error, data) => {
if (error) {
console.error('Error:', error);
return;
}
processData(data, (error, processedData) => {
if (error) {
console.error('Error:', error);
return;
}
saveData(processedData, (error, result) => {
if (error) {
console.error('Error:', error);
return;
}
console.log('Result:', result);
});
});
});回调地狱的真实场景
// 真实业务:登录 -> 拉取用户资料 -> 拉取用户订单 -> 拉取订单详情
login(username, password, (err, token) => {
if (err) return handleError(err);
getUserProfile(token, (err, profile) => {
if (err) return handleError(err);
getUserOrders(profile.userId, (err, orders) => {
if (err) return handleError(err);
getOrderDetail(orders[0].id, (err, detail) => {
if (err) return handleError(err);
// 到这里已经缩进了五层,任何改动都很痛苦
renderPage(profile, orders, detail);
});
});
});
});回调地狱的核心问题:
正是这些问题催生了 Promise。
Promise
Promise 的概念:
类比:取餐号
在快餐店点餐后,你会拿到一个取餐号(Promise 对象)。此时餐还没做好(pending),你不必站在柜台前干等,可以先去找座位。餐做好了会叫号(fulfilled),如果这道菜卖完了做不了也会通知你退款(rejected)。取餐号本身不是餐,而是"未来能拿到餐的凭证"。
Promise 的方法:
Promise 解决了什么:
代码示例
Promise 基础示例
// 创建 Promise
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve({ name: 'Alice', age: 25 });
} else {
reject(new Error('Failed to fetch data'));
}
}, 1000);
});
// 使用 Promise
promise
.then(data => {
console.log('Data:', data);
return data;
})
.catch(error => {
console.error('Error:', error);
})
.finally(() => {
console.log('Operation completed');
});Promise 的三种状态
// pending -> fulfilled
const p1 = new Promise((resolve) => {
setTimeout(() => resolve('成功值'), 500);
});
// pending -> rejected
const p2 = new Promise((_, reject) => {
setTimeout(() => reject(new Error('失败原因')), 500);
});
// 已经确定状态的 Promise,状态凝固后 resolve/reject 再调用无效
const p3 = new Promise((resolve, reject) => {
resolve('第一次');
resolve('第二次'); // 无效,被忽略
reject(new Error('也无效')); // 无效,被忽略
});
p3.then(v => console.log(v)); // 输出:第一次
// 快捷创建
const immediate = Promise.resolve(42);
const failed = Promise.reject(new Error('立即失败'));
immediate.then(v => console.log(v)); // 42
failed.catch(e => console.log(e.message)); // 立即失败Promise 链式调用
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: 'Alice', age: 25 });
}, 1000);
});
}
function processData(data) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ...data, processed: true });
}, 500);
});
}
function saveData(data) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ success: true, data });
}, 300);
});
}
// 链式调用:把回调地狱拉平成流水线
fetchData()
.then(data => {
console.log('Fetched:', data);
return processData(data);
})
.then(data => {
console.log('Processed:', data);
return saveData(data);
})
.then(result => {
console.log('Saved:', result);
})
.catch(error => {
// 链条上任意环节出错都会走到这里
console.error('Error:', error);
});then 中返回值的三种情况
Promise.resolve(1)
// 情况一:返回普通值,会被包装成 fulfilled 的 Promise
.then(v => {
console.log('第一步:', v); // 1
return v + 1;
})
// 情况二:返回一个 Promise,会等它确定后再传递结果
.then(v => {
console.log('第二步:', v); // 2
return new Promise(resolve => setTimeout(() => resolve(v * 10), 200));
})
// 情况三:抛出错误,会让返回的 Promise 变为 rejected
.then(v => {
console.log('第三步:', v); // 20
throw new Error('故意抛错');
})
.then(v => {
console.log('这一步不会执行:', v);
})
.catch(err => {
console.log('捕获到错误:', err.message); // 故意抛错
});Promise.all 并发执行
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: `User ${id}` });
}, 1000);
});
}
function fetchPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, userId, title: 'Post 1' },
{ id: 2, userId, title: 'Post 2' }
]);
}, 800);
});
}
function fetchComments(postId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, postId, text: 'Comment 1' },
{ id: 2, postId, text: 'Comment 2' }
]);
}, 600);
});
}
// 并发执行:三个请求同时发出,总耗时约等于最慢的那个(1000ms)
Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
])
.then(([user, posts, comments]) => {
console.log('User:', user);
console.log('Posts:', posts);
console.log('Comments:', comments);
})
.catch(error => {
// 只要有一个失败,整体立即 reject
console.error('Error:', error);
});Promise.all 的快速失败特性
// Promise.all 是"快速失败"的:任意一个 reject,整体立即 reject
// 其余尚未完成的 Promise 并不会被取消,只是结果被忽略
const tasks = [
Promise.resolve('任务 A 成功'),
Promise.reject(new Error('任务 B 失败')),
new Promise(resolve => setTimeout(() => resolve('任务 C 成功'), 1000))
];
Promise.all(tasks)
.then(results => console.log('全部成功:', results))
.catch(error => console.error('有失败:', error.message));
// 输出:有失败:任务 B 失败
// 注意:任务 C 仍会在 1 秒后完成,只是没人接收它的结果Promise.race 超时处理
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ data: 'Some data' });
}, 2000);
});
}
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Timeout'));
}, ms);
});
}
// 使用 Promise.race 实现超时:谁先确定状态就用谁的结果
Promise.race([
fetchData(),
timeout(1000)
])
.then(data => {
console.log('Data:', data);
})
.catch(error => {
// 数据请求要 2 秒,超时只要 1 秒,所以这里会捕获 Timeout
console.error('Error:', error.message);
});Promise.allSettled 等待所有完成
const promises = [
Promise.resolve(1),
Promise.reject(new Error('Failed')),
Promise.resolve(3)
];
// allSettled 不会因为某个失败而中断,会等所有都确定状态
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index}: ${result.value}`);
} else {
console.log(`Promise ${index}: ${result.reason.message}`);
}
});
});
// 输出:
// Promise 0: 1
// Promise 1: Failed
// Promise 2: 3Promise.any 返回第一个成功
// any:只要有一个成功就成功,全部失败才失败(抛出 AggregateError)
const mirrors = [
fetch('https://mirror1.example.com/file'),
fetch('https://mirror2.example.com/file'),
fetch('https://mirror3.example.com/file')
];
Promise.any(mirrors)
.then(response => {
console.log('最快返回的镜像:', response.url);
})
.catch(error => {
// 所有镜像都失败时
console.error('全部失败:', error.errors);
});Promise 组合方法行为对比表
| 方法 | 成功条件 | 失败条件 | 返回结果 | 典型用途 |
| --- | --- | --- | --- | --- |
| Promise.all | 全部成功 | 任意一个失败 | 成功值组成的数组 | 多个必须全部成功的并发请求 |
| Promise.allSettled | 总是"成功"(除非本身报错) | 从不 reject | 每项 status/value/reason 的数组 | 批量任务,需知道每个成败 |
| Promise.race | 第一个确定的是成功 | 第一个确定的是失败 | 第一个确定者的值或错误 | 超时控制、竞速 |
| Promise.any | 任意一个成功 | 全部失败 | 第一个成功值 | 多镜像取最快、容错请求 |
手写一个简易 Promise
// 通过手写理解 Promise 的内部机制:状态机 + 回调队列
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
// 状态还是 pending 时先把回调存起来,确定状态后再执行
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.status = 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 : v => v;
onRejected = typeof onRejected === 'function'
? onRejected
: err => { throw err; };
// then 必须返回新的 Promise 才能链式调用
const promise2 = new MyPromise((resolve, reject) => {
const handleFulfilled = () => {
// 用 setTimeout 模拟微任务,保证异步执行
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (err) {
reject(err);
}
}, 0);
};
const handleRejected = () => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (err) {
reject(err);
}
}, 0);
};
if (this.status === FULFILLED) {
handleFulfilled();
} else if (this.status === REJECTED) {
handleRejected();
} else {
// pending:先存入队列,等 resolve/reject 时触发
this.onFulfilledCallbacks.push(handleFulfilled);
this.onRejectedCallbacks.push(handleRejected);
}
});
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(callback) {
return this.then(
value => { callback(); return value; },
reason => { callback(); throw reason; }
);
}
static resolve(value) {
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}
// 测试
new MyPromise((resolve) => {
setTimeout(() => resolve('手写 Promise 成功'), 300);
})
.then(v => {
console.log(v);
return v.length;
})
.then(len => console.log('长度:', len));手写 Promise.all
function myAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;
const total = promises.length;
if (total === 0) {
resolve(results);
return;
}
promises.forEach((p, index) => {
// 用 Promise.resolve 包装,兼容传入非 Promise 的值
Promise.resolve(p).then(
value => {
results[index] = value; // 按原始顺序放置结果
completed++;
if (completed === total) {
resolve(results);
}
},
reason => reject(reason) // 任意失败立即整体失败
);
});
});
}
myAll([
Promise.resolve(1),
new Promise(r => setTimeout(() => r(2), 100)),
Promise.resolve(3)
]).then(res => console.log('myAll 结果:', res)); // [1, 2, 3]手写 Promise.allSettled
function myAllSettled(promises) {
return new Promise((resolve) => {
const results = [];
let completed = 0;
const total = promises.length;
if (total === 0) {
resolve(results);
return;
}
promises.forEach((p, index) => {
Promise.resolve(p).then(
value => {
results[index] = { status: 'fulfilled', value };
if (++completed === total) resolve(results);
},
reason => {
results[index] = { status: 'rejected', reason };
if (++completed === total) resolve(results);
}
);
});
});
}事件循环与宏任务、微任务
事件循环(Event Loop)的概念:
事件循环是 JavaScript 运行时协调同步代码、异步回调、渲染的核心机制。它决定了"什么时候执行什么代码"。理解事件循环,才能准确预测异步代码的执行顺序,避免各种诡异的 bug。
核心组成:
类比:餐厅点餐与叫号系统
再回到餐厅的比喻。服务员(主线程)先把手上正在做的事(同步代码,调用栈)做完。做完后,他会优先处理"插队的紧急小事"(微任务队列)——比如给刚上完菜的桌子递一下餐巾纸,这些琐事必须一次性全部处理干净。全部微任务处理完后,可能需要整理一下餐厅(渲染)。然后他才会去看"叫号大屏"(宏任务队列),叫下一位排队的客人(一个宏任务)。每处理完一个宏任务,又会把积攒的紧急小事(微任务)全部清空,如此循环往复。
关键规则(务必记住):
一句话总结:每执行完一个宏任务,就把当前所有微任务清空。
为什么区分宏任务和微任务重要:
宏任务与微任务优先级表
| 类别 | 常见来源 | 执行时机 | 优先级 |
| --- | --- | --- | --- |
| 同步代码 | 普通函数调用 | 立即,占用调用栈 | 最高 |
| 微任务 | Promise.then/catch/finally、queueMicrotask、MutationObserver、await 之后的代码 | 当前宏任务结束后立即全部清空 | 高 |
| 宏任务 | setTimeout、setInterval、setImmediate(Node)、I/O、UI 事件、MessageChannel | 微任务清空后每轮取一个 | 低 |
| 渲染 | 浏览器重绘 | 微任务清空后、下一个宏任务前(约 16.6ms 一帧) | 介于两者之间 |
执行顺序推演示例一
console.log('1 同步开始');
setTimeout(() => {
console.log('2 宏任务 setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('3 微任务 then');
});
console.log('4 同步结束');
// 推演过程:
// - 同步执行:打印 1,注册 setTimeout(进宏任务队列),注册 then(进微任务队列),打印 4
// - 调用栈清空,开始清微任务:打印 3
// - 微任务清空,取一个宏任务:打印 2
//
// 最终输出顺序:
// 1 同步开始
// 4 同步结束
// 3 微任务 then
// 2 宏任务 setTimeout执行顺序推演示例二(嵌套)
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve()
.then(() => {
console.log('promise1');
// 在微任务里再注册微任务,本轮会继续清空
Promise.resolve().then(() => console.log('promise1-nested'));
})
.then(() => {
console.log('promise2');
});
console.log('script end');
// 输出顺序:
// script start
// script end
// promise1
// promise1-nested <- 注意:新产生的微任务在本轮就被清空
// promise2
// setTimeout <- 宏任务最后执行执行顺序推演示例三(async/await 混合)
async function async1() {
console.log('async1 start');
await async2();
// await 后面的代码相当于放进微任务队列
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise((resolve) => {
console.log('promise executor'); // executor 是同步执行的
resolve();
}).then(() => {
console.log('promise then');
});
console.log('script end');
// 输出顺序:
// script start
// async1 start
// async2
// promise executor
// script end
// async1 end <- await 之后的代码作为微任务
// promise then
// setTimeoutNode.js 与浏览器事件循环的差异
// Node.js 特有的两个 API
// process.nextTick 优先级甚至高于 Promise 微任务
// setImmediate 属于 check 阶段的宏任务
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('同步代码');
// Node 中典型输出:
// 同步代码
// nextTick <- nextTick 队列优先于微任务队列
// promise
// setTimeout / setImmediate 顺序在此场景下不确定,取决于进入事件循环的时机
//
// 浏览器没有 process.nextTick 和 setImmediate,
// 微任务统一由 Promise / queueMicrotask 提供用 queueMicrotask 手动调度微任务
console.log('开始');
queueMicrotask(() => {
console.log('这是一个微任务');
});
Promise.resolve().then(() => console.log('Promise 微任务'));
console.log('结束');
// 输出:
// 开始
// 结束
// 这是一个微任务
// Promise 微任务async/await
async/await 的概念:
使用方法:
async/await 相对 Promise 的优势:
代码示例
async/await 基础示例
async function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: 'Alice', age: 25 });
}, 1000);
});
}
async function main() {
try {
const data = await fetchData();
console.log('Data:', data);
} catch (error) {
console.error('Error:', error);
}
}
main();async 函数一定返回 Promise
async function returnValue() {
return 42; // 返回普通值
}
// 即使返回普通值,调用结果也是 Promise
returnValue().then(v => console.log('拿到值:', v)); // 42
async function throwError() {
throw new Error('出错了'); // 抛出错误
}
// 抛出的错误变成 rejected 的 Promise
throwError().catch(e => console.log('拿到错误:', e.message)); // 出错了async/await 错误处理
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async function main() {
try {
const user = await fetchUser(1);
console.log('User:', user);
} catch (error) {
console.error('Failed to fetch user:', error.message);
}
}
main();优雅的错误处理:await-to 模式
// 借鉴 Go 的错误处理风格,避免大量 try/catch 嵌套
function to(promise) {
return promise
.then(data => [null, data])
.catch(err => [err, null]);
}
async function main() {
const [err1, user] = await to(fetchUser(1));
if (err1) {
console.error('获取用户失败:', err1.message);
return;
}
const [err2, orders] = await to(fetchOrders(user.id));
if (err2) {
console.error('获取订单失败:', err2.message);
return;
}
console.log('用户与订单:', user, orders);
}async/await 并发执行
async function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: `User ${id}` });
}, 1000);
});
}
async function fetchPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, userId, title: 'Post 1' },
{ id: 2, userId, title: 'Post 2' }
]);
}, 800);
});
}
async function fetchComments(postId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, postId, text: 'Comment 1' },
{ id: 2, postId, text: 'Comment 2' }
]);
}, 600);
});
}
// 并发执行:三个请求彼此无依赖,用 Promise.all 同时发起
async function main() {
try {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
console.log('User:', user);
console.log('Posts:', posts);
console.log('Comments:', comments);
} catch (error) {
console.error('Error:', error);
}
}
main();async/await 顺序执行
async function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: `User ${id}` });
}, 1000);
});
}
async function fetchPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, userId, title: 'Post 1' }
]);
}, 800);
});
}
// 顺序执行:后一步依赖前一步的结果时才用串行
async function main() {
try {
const user = await fetchUser(1);
console.log('User:', user);
const posts = await fetchPosts(user.id);
console.log('Posts:', posts);
} catch (error) {
console.error('Error:', error);
}
}
main();常见误区:不必要的串行
// 反例:两个请求彼此无依赖,却写成串行,白白多等待
async function badExample() {
const user = await fetchUser(1); // 等 1 秒
const config = await fetchConfig(); // 再等 1 秒
// 总耗时约 2 秒
return { user, config };
}
// 正例:无依赖就并发,总耗时约等于较慢的那个
async function goodExample() {
const [user, config] = await Promise.all([
fetchUser(1),
fetchConfig()
]);
// 总耗时约 1 秒
return { user, config };
}在循环中使用 await
// 场景一:需要严格顺序执行(例如后一次依赖前一次),用 for...of + await
async function processSequentially(items) {
const results = [];
for (const item of items) {
const result = await processItem(item); // 一个接一个
results.push(result);
}
return results;
}
// 场景二:彼此无依赖,希望并发,用 map + Promise.all
async function processConcurrently(items) {
const results = await Promise.all(
items.map(item => processItem(item))
);
return results;
}
// 陷阱:forEach 不会等待 await,回调是异步的但 forEach 本身不 await
async function wrongLoop(items) {
items.forEach(async (item) => {
await processItem(item); // forEach 不会等这些完成
});
console.log('这行会在所有 processItem 完成前就打印'); // 错误预期
}async/await 的底层原理
核心结论:async/await 是"生成器(Generator)+ 自动执行器(协程)"的语法糖。
在 async/await 出现之前,社区就用生成器配合执行器(如 co 库)模拟出了类似的写法。async/await 相当于把这套机制内置到了语言层面。
理解链条:
协程的概念:
协程是一种可以主动让出执行权、之后再从让出点恢复的"轻量线程"。JavaScript 的生成器提供了协程的基础能力。async 函数就是运行在一个自动驱动的协程上:遇到 await 就挂起,Promise 完成后事件循环把它唤醒继续跑。
生成器(Generator)
生成器的概念:
生成器基础
function* simpleGenerator() {
console.log('开始');
yield 1; // 第一次 next 执行到这里暂停
console.log('第一次恢复');
yield 2; // 第二次 next 执行到这里暂停
console.log('第二次恢复');
return 3; // 第三次 next 结束
}
const gen = simpleGenerator();
console.log(gen.next()); // 打印"开始",返回 { value: 1, done: false }
console.log(gen.next()); // 打印"第一次恢复",返回 { value: 2, done: false }
console.log(gen.next()); // 打印"第二次恢复",返回 { value: 3, done: true }
console.log(gen.next()); // 返回 { value: undefined, done: true }通过 next 向生成器传值
function* dialogue() {
const name = yield '你叫什么名字?';
const age = yield `你好 ${name},你多大了?`;
return `${name} 今年 ${age} 岁`;
}
const g = dialogue();
console.log(g.next().value); // 你叫什么名字?
console.log(g.next('Alice').value); // 你好 Alice,你多大了?(把 Alice 传回给 name)
console.log(g.next(25).value); // Alice 今年 25 岁(把 25 传回给 age)用生成器 + 执行器模拟 async/await
// 这段代码揭示了 async/await 的本质
function run(generatorFunc) {
const gen = generatorFunc();
function step(nextValue) {
// 把上一步的结果喂回生成器,拿到下一个 yield 的 Promise
const { value, done } = gen.next(nextValue);
if (done) {
return Promise.resolve(value);
}
// 等待 yield 出来的 Promise 完成,再驱动下一步
return Promise.resolve(value).then(
resolved => step(resolved),
err => gen.throw(err) // 让错误在生成器内部的 try/catch 捕获
);
}
return step();
}
function fetchNumber(n) {
return new Promise(resolve => setTimeout(() => resolve(n * 2), 300));
}
// 用 yield 代替 await,效果几乎一样
run(function* () {
const a = yield fetchNumber(1); // 相当于 await
console.log('a =', a); // 2
const b = yield fetchNumber(a); // 相当于 await
console.log('b =', b); // 4
return a + b;
}).then(total => console.log('total =', total)); // 6
// 对比等价的 async/await 写法:
// async function main() {
// const a = await fetchNumber(1);
// const b = await fetchNumber(a);
// return a + b;
// }生成器实现惰性无限序列
// 生成器可以表示"无限"序列,因为它是惰性求值的
function* naturalNumbers() {
let n = 1;
while (true) {
yield n++; // 每次 next 才生成下一个,不会真的死循环
}
}
const nums = naturalNumbers();
console.log(nums.next().value); // 1
console.log(nums.next().value); // 2
console.log(nums.next().value); // 3
// 取前 n 个的工具
function take(iterator, count) {
const result = [];
for (let i = 0; i < count; i++) {
result.push(iterator.next().value);
}
return result;
}
console.log(take(naturalNumbers(), 5)); // [1, 2, 3, 4, 5]异步迭代器与 for await...of
异步迭代器的概念:
为什么需要它:
当数据源是"一批一批异步到来"的(如分页接口、流式响应、逐块读取文件),异步迭代器提供了统一、优雅的消费方式,配合 for await...of 写起来像遍历普通数组一样自然。
异步生成器基础
// async function* 声明异步生成器
async function* asyncCounter(max) {
for (let i = 1; i <= max; i++) {
// 模拟每次异步获取数据
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function main() {
// for await...of 自动等待每一个 yield 的值
for await (const num of asyncCounter(3)) {
console.log('收到:', num); // 每隔 200ms 打印 1, 2, 3
}
console.log('遍历完成');
}
main();用异步迭代器实现自动分页拉取
// 真实场景:接口分页,每页返回 items 和是否有下一页
async function fetchPage(page) {
const response = await fetch(`/api/list?page=${page}`);
return response.json(); // { items: [...], hasNext: true/false }
}
// 把"分页接口"封装成一个异步可迭代对象
async function* paginate() {
let page = 1;
let hasNext = true;
while (hasNext) {
const data = await fetchPage(page);
for (const item of data.items) {
yield item; // 逐条产出
}
hasNext = data.hasNext;
page++;
}
}
async function main() {
let count = 0;
// 消费方完全不需要关心分页细节
for await (const item of paginate()) {
console.log('处理条目:', item.id);
count++;
if (count >= 100) break; // 可以随时中断,不会多拉数据
}
console.log('共处理', count, '条');
}
main();自定义 Symbol.asyncIterator
// 让任意对象支持 for await...of
const asyncRange = {
from: 1,
to: 5,
[Symbol.asyncIterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return new Promise(resolve => {
setTimeout(() => {
if (current <= last) {
resolve({ value: current++, done: false });
} else {
resolve({ value: undefined, done: true });
}
}, 100);
});
}
};
}
};
(async () => {
for await (const value of asyncRange) {
console.log(value); // 每隔 100ms 打印 1, 2, 3, 4, 5
}
})();AbortController 取消异步请求
为什么需要取消:
很多异步操作发起后,我们可能不再需要它的结果了:用户快速切换标签页、搜索框内容频繁变化、组件被卸载。如果不取消,不仅浪费带宽和计算,还可能导致"竞态条件"(旧请求的结果覆盖了新请求)。AbortController 提供了标准的取消机制。
核心 API:
基本用法:可取消的 fetch
const controller = new AbortController();
fetch('/api/large-data', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log('数据:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求被取消');
} else {
console.error('请求失败:', error);
}
});
// 3 秒后如果还没完成就取消
setTimeout(() => controller.abort(), 3000);用 AbortController 实现请求超时
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
return fetch(url, { ...options, signal: controller.signal })
.finally(() => clearTimeout(timer)); // 完成后清理定时器
}
fetchWithTimeout('/api/data', {}, 3000)
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求超时被中断');
}
});解决搜索竞态:取消上一次请求
let currentController = null;
async function search(keyword) {
// 有新请求时,先取消上一个还在进行的请求
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(keyword)}`,
{ signal: currentController.signal }
);
const results = await response.json();
renderResults(results); // 只有最新请求的结果会被渲染
} catch (error) {
if (error.name !== 'AbortError') {
console.error('搜索失败:', error);
}
// AbortError 说明是被新请求取消的,静默忽略即可
}
}并发控制与限制池
为什么要限制并发:
假设要发起 1000 个请求,如果一次性全部发出,会瞬间打满浏览器的连接数限制、拖垮服务器、甚至触发限流。合理的做法是维持一个"并发窗口",例如同时最多 5 个请求在跑,一个完成就补一个进来,直到全部完成。这就是并发限制池。
简单的并发控制(原始实现)
async function fetchUrl(url) {
const response = await fetch(url);
return response.json();
}
async function fetchAll(urls, concurrency = 3) {
const results = [];
const executing = [];
for (const url of urls) {
const promise = fetchUrl(url).then(result => {
executing.splice(executing.indexOf(promise), 1);
return result;
});
executing.push(promise);
results.push(promise);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
const urls = [
'https://api.example.com/data1',
'https://api.example.com/data2',
'https://api.example.com/data3',
'https://api.example.com/data4',
'https://api.example.com/data5'
];
fetchAll(urls, 3)
.then(results => console.log('Results:', results))
.catch(error => console.error('Error:', error));更健壮的并发池(保持结果顺序)
// 用"工人(worker)"模型实现并发池:开 n 个工人,每个工人不断从任务队列取任务
async function runWithConcurrency(tasks, concurrency = 5) {
const results = new Array(tasks.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < tasks.length) {
const currentIndex = nextIndex++; // 领取一个任务下标
try {
results[currentIndex] = await tasks[currentIndex]();
} catch (err) {
results[currentIndex] = { error: err };
}
}
}
// 启动 concurrency 个工人并行消费
const workers = [];
for (let i = 0; i < concurrency; i++) {
workers.push(worker());
}
await Promise.all(workers);
return results; // 结果按任务原始顺序排列
}
// 使用:tasks 是"返回 Promise 的函数"数组(惰性,避免提前发起)
const ids = Array.from({ length: 20 }, (_, i) => i + 1);
const tasks = ids.map(id => () => fetch(`/api/item/${id}`).then(r => r.json()));
runWithConcurrency(tasks, 4).then(results => {
console.log('全部完成,共', results.length, '项');
});带并发上限的通用调度器类
class TaskPool {
constructor(concurrency = 5) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
// 加入一个返回 Promise 的任务函数,返回该任务的结果 Promise
add(taskFn) {
return new Promise((resolve, reject) => {
this.queue.push({ taskFn, resolve, reject });
this._schedule();
});
}
_schedule() {
// 只要还有空位且队列非空,就取任务执行
while (this.running < this.concurrency && this.queue.length > 0) {
const { taskFn, resolve, reject } = this.queue.shift();
this.running++;
Promise.resolve()
.then(taskFn)
.then(resolve, reject)
.finally(() => {
this.running--;
this._schedule(); // 有任务完成,尝试补位
});
}
}
}
// 使用
const pool = new TaskPool(3);
const jobs = Array.from({ length: 10 }, (_, i) => i);
Promise.all(
jobs.map(i => pool.add(() =>
new Promise(resolve => setTimeout(() => {
console.log('完成任务', i);
resolve(i);
}, 500))
))
).then(() => console.log('全部任务完成'));防抖与节流
防抖(debounce)和节流(throttle)是控制高频触发函数执行频率的两种经典技术,常用于搜索输入、滚动监听、窗口 resize、按钮防连点等场景。
防抖(debounce):
节流(throttle):
防抖实现
function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
// 每次触发都清除上一次的定时器,重新计时
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 使用:搜索框输入停止 300ms 后才真正发起请求
const onInput = debounce((event) => {
console.log('发起搜索:', event.target.value);
}, 300);
// searchInput.addEventListener('input', onInput);支持立即执行的防抖
function debounce(fn, delay = 300, immediate = false) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
// 第一次立即执行,之后进入防抖
fn.apply(this, args);
}
timer = setTimeout(() => {
if (!immediate) {
fn.apply(this, args);
}
timer = null;
}, delay);
};
}节流实现(时间戳版)
function throttle(fn, interval = 300) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
// 距离上次执行超过 interval 才执行
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 使用:滚动时最多每 200ms 处理一次
const onScroll = throttle(() => {
console.log('处理滚动位置:', window.scrollY);
}, 200);
// window.addEventListener('scroll', onScroll);节流实现(定时器版,保证最后一次执行)
function throttle(fn, interval = 300) {
let timer = null;
return function (...args) {
if (timer) return; // 定时器存在说明冷却期内,直接忽略
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, interval);
};
}防抖与节流对比
| 维度 | 防抖 debounce | 节流 throttle |
| --- | --- | --- |
| 核心思想 | 触发后等待,期间再触发就重新计时 | 固定时间窗口内只执行一次 |
| 执行时机 | 高频触发停止后执行一次 | 高频触发过程中周期性执行 |
| 类比 | 电梯门等最后一个人 | 水龙头限流匀速出水 |
| 适用场景 | 搜索联想、输入校验、resize 收尾 | 滚动加载、鼠标移动、按钮防连点 |
| 是否保证执行 | 停止触发后才执行 | 过程中稳定执行 |
真实案例一:电商详情页并发拉取
场景描述:
打开一个电商商品详情页,需要同时拉取:用户信息、购物车、商品详情、推荐商品、评价列表。这些接口彼此独立,如果串行请求会非常慢。我们对比串行与并行两种方案的耗时。
串行方案(反例)
async function loadPageSerial(productId) {
const start = Date.now();
const user = await fetchUser(); // 200ms
const cart = await fetchCart(); // 150ms
const product = await fetchProduct(productId); // 300ms
const recommends = await fetchRecommends(productId); // 250ms
const reviews = await fetchReviews(productId); // 180ms
console.log('串行总耗时:', Date.now() - start, 'ms');
// 约 200+150+300+250+180 = 1080ms
return { user, cart, product, recommends, reviews };
}并行方案(正例)
async function loadPageParallel(productId) {
const start = Date.now();
// 五个请求同时发出,总耗时约等于最慢的那个(300ms)
const [user, cart, product, recommends, reviews] = await Promise.all([
fetchUser(),
fetchCart(),
fetchProduct(productId),
fetchRecommends(productId),
fetchReviews(productId)
]);
console.log('并行总耗时:', Date.now() - start, 'ms');
// 约 300ms
return { user, cart, product, recommends, reviews };
}混合方案:部分依赖 + 部分并行
// 实际业务往往是"部分请求依赖前置结果,其余可并行"
async function loadPageMixed(productId) {
// 第一步:商品详情必须先拿到(后续推荐依赖它的分类)
const product = await fetchProduct(productId);
// 第二步:以下请求彼此独立,并行发起
const [recommends, reviews, related] = await Promise.all([
fetchRecommends(product.categoryId),
fetchReviews(productId),
fetchRelated(product.brandId)
]);
return { product, recommends, reviews, related };
}串行 vs 并行耗时对比表
| 方案 | 请求方式 | 各请求耗时 | 总耗时 | 相对串行提速 |
| --- | --- | --- | --- | --- |
| 串行 | 一个接一个 await | 200/150/300/250/180 | 约 1080ms | 基准 |
| 并行 | Promise.all 同时发起 | 同上(并发) | 约 300ms | 约 3.6 倍 |
| 混合 | 先拿商品再并行其余 | 300 + max(其余) | 约 550ms | 约 2 倍 |
结论:无依赖的请求一定要并行;有依赖时用混合方案,把能并行的部分尽量并行。
真实案例二:搜索框防抖 + 取消
场景描述:
用户在搜索框快速输入时,如果每次按键都发请求,会产生大量无效请求,还可能出现"竞态"——先发出的慢请求后返回,覆盖了后发出的快请求结果。解决方案是:防抖减少请求数量,AbortController 取消过期请求。
function createSearch(renderResults) {
let controller = null;
// 内部真正的搜索逻辑
async function doSearch(keyword) {
if (!keyword.trim()) {
renderResults([]);
return;
}
// 取消上一个未完成的请求,避免竞态
if (controller) controller.abort();
controller = new AbortController();
try {
const res = await fetch(
`/api/search?q=${encodeURIComponent(keyword)}`,
{ signal: controller.signal }
);
const data = await res.json();
renderResults(data.results);
} catch (err) {
if (err.name !== 'AbortError') {
console.error('搜索出错:', err);
}
}
}
// 用防抖包裹,输入停止 300ms 后才真正搜索
const debouncedSearch = debounce(doSearch, 300);
return function onInput(event) {
debouncedSearch(event.target.value);
};
}
// 使用
// const handler = createSearch(results => console.log('渲染:', results));
// searchInput.addEventListener('input', handler);真实案例三:大列表分批请求(瀑布流)
场景描述:
需要为 1000 个商品逐个拉取库存信息,直接并发 1000 个请求会打垮服务。改用分批 + 并发控制:每批并发若干个,一批完成再进行下一批,并可展示进度。
分批处理(chunk 方式)
// 把大数组切成固定大小的批次
function chunk(array, size) {
const result = [];
for (let i = 0; i < array.length; i += size) {
result.push(array.slice(i, i + size));
}
return result;
}
async function fetchStockInBatches(productIds, batchSize = 10) {
const batches = chunk(productIds, batchSize);
const allResults = [];
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
// 每批内部并发,批与批之间串行
const batchResults = await Promise.all(
batch.map(id => fetchStock(id))
);
allResults.push(...batchResults);
console.log(`进度:${allResults.length}/${productIds.length}`);
}
return allResults;
}
const ids = Array.from({ length: 1000 }, (_, i) => i + 1);
fetchStockInBatches(ids, 10).then(stocks => {
console.log('全部库存拉取完成,共', stocks.length, '项');
});瀑布流滚动加载
class InfiniteScroll {
constructor({ container, loadMore, threshold = 200 }) {
this.container = container;
this.loadMore = loadMore; // 返回 Promise 的加载函数
this.threshold = threshold; // 距底部多少像素时触发
this.loading = false;
this.finished = false;
this.page = 1;
// 滚动监听用节流,避免高频触发
this.onScroll = throttle(this._handleScroll.bind(this), 200);
window.addEventListener('scroll', this.onScroll);
}
async _handleScroll() {
if (this.loading || this.finished) return;
const scrollBottom = window.innerHeight + window.scrollY;
const pageHeight = document.body.offsetHeight;
if (pageHeight - scrollBottom < this.threshold) {
this.loading = true;
try {
const { items, hasMore } = await this.loadMore(this.page);
this._render(items);
this.page++;
if (!hasMore) this.finished = true;
} catch (err) {
console.error('加载失败:', err);
} finally {
this.loading = false;
}
}
}
_render(items) {
items.forEach(item => {
const el = document.createElement('div');
el.textContent = item.title;
this.container.appendChild(el);
});
}
destroy() {
window.removeEventListener('scroll', this.onScroll);
}
}真实案例四:接口聚合与降级
场景描述:
首页需要聚合多个数据模块,但并非每个模块都是关键。用 Promise.allSettled 让部分模块失败时不影响整体,失败的模块降级为默认内容。
async function loadHomePage() {
const results = await Promise.allSettled([
fetchBanners(), // 轮播图
fetchHotProducts(), // 热销商品
fetchNews(), // 资讯
fetchActivities() // 活动
]);
// 逐个处理,失败的降级为空数组或默认值
const [banners, hotProducts, news, activities] = results.map((r, i) => {
if (r.status === 'fulfilled') {
return r.value;
}
console.warn(`模块 ${i} 加载失败,使用降级数据:`, r.reason.message);
return []; // 降级
});
return { banners, hotProducts, news, activities };
}请求重试与指数退避
为什么需要重试:
网络请求可能因为瞬时抖动、服务临时不可用而失败。对于幂等的读请求,自动重试几次往往就能成功。指数退避(每次重试间隔翻倍)可以避免在服务繁忙时反复冲击。
基础重试
async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
} catch (error) {
if (i === retries - 1) {
throw error; // 最后一次仍失败,抛出
}
// 指数退避:1s, 2s, 4s ...
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
fetchWithRetry('https://api.example.com/data')
.then(data => console.log('Data:', data))
.catch(error => console.error('Error:', error));带抖动(jitter)的指数退避
// 加入随机抖动,避免大量客户端在同一时刻集中重试(惊群效应)
async function fetchWithBackoff(url, options = {}, {
retries = 5,
baseDelay = 500,
maxDelay = 10000
} = {}) {
let attempt = 0;
while (true) {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
attempt++;
if (attempt > retries) throw err;
// 指数增长并加入随机抖动,同时设上限
const expDelay = Math.min(baseDelay * 2 ** (attempt - 1), maxDelay);
const jitter = Math.random() * expDelay * 0.3;
const delay = expDelay + jitter;
console.log(`第 ${attempt} 次重试,等待 ${Math.round(delay)}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
}其他实用异步工具
睡眠函数 sleep
// 把 setTimeout 包装成可 await 的 sleep
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function demo() {
console.log('开始');
await sleep(1000);
console.log('1 秒后');
await sleep(500);
console.log('再过 0.5 秒');
}给任意 Promise 加超时
function withTimeout(promise, ms, message = '操作超时') {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error(message)), ms);
});
return Promise.race([promise, timeout]);
}
// 使用
withTimeout(fetch('/api/slow').then(r => r.json()), 3000)
.then(data => console.log(data))
.catch(err => console.error(err.message)); // 超时会打印"操作超时"简单的异步结果缓存(去重同一请求)
function memoizeAsync(fn) {
const cache = new Map();
return function (key) {
if (cache.has(key)) {
return cache.get(key); // 命中缓存,直接返回同一个 Promise
}
const promise = fn(key).catch(err => {
cache.delete(key); // 失败时清除缓存,允许下次重试
throw err;
});
cache.set(key, promise);
return promise;
};
}
const getUser = memoizeAsync(id =>
fetch(`/api/users/${id}`).then(r => r.json())
);
// 同一 id 的并发调用会复用同一个请求,避免重复发送
getUser(1);
getUser(1); // 不会再发一次请求串行执行 Promise 数组(reduce 技巧)
// 用 reduce 把一组任务串成链式,保证严格顺序执行
function runSequentially(taskFns) {
return taskFns.reduce(
(chain, taskFn) => chain.then(results =>
taskFn().then(result => [...results, result])
),
Promise.resolve([])
);
}
const tasks = [
() => sleep(300).then(() => 'A'),
() => sleep(100).then(() => 'B'),
() => sleep(200).then(() => 'C')
];
runSequentially(tasks).then(results => {
console.log(results); // ['A', 'B', 'C'](严格按顺序)
});常见坑与陷阱
坑一:忘记 await,拿到的是 Promise 而不是值
async function bug1() {
const user = fetchUser(1); // 忘了 await
console.log(user.name); // undefined,因为 user 是 Promise 对象
}
async function fixed1() {
const user = await fetchUser(1);
console.log(user.name); // 正确
}坑二:在 forEach 里用 await 不会等待
// forEach 不感知 async 回调,不会等待
async function bug2(ids) {
ids.forEach(async (id) => {
await deleteItem(id);
});
console.log('删除完成'); // 会立即打印,此时删除可能都没开始完成
}
// 用 for...of 或 Promise.all 修复
async function fixed2(ids) {
await Promise.all(ids.map(id => deleteItem(id))); // 并发
console.log('删除完成'); // 真正完成后才打印
}坑三:未处理的 Promise rejection
// 没有 catch 的 Promise 抛错会产生 UnhandledPromiseRejection 警告
fetchUser(1).then(user => {
throw new Error('处理出错'); // 没有 catch 接住
});
// 修复:总是加上 catch,或用 try/catch 包裹 await
fetchUser(1)
.then(user => { throw new Error('处理出错'); })
.catch(err => console.error('已捕获:', err.message));坑四:Promise 构造器里再包一层没必要
// 反例:多此一举地用 new Promise 包裹一个已有的 Promise
function bad() {
return new Promise((resolve, reject) => {
fetch('/api').then(resolve).catch(reject);
});
}
// 正例:直接返回即可
function good() {
return fetch('/api');
}坑五:async 函数里的错误吞掉
// 反例:catch 后什么都不做,错误被静默吞掉,难以排查
async function bad() {
try {
await risky();
} catch (e) {
// 空 catch,问题被隐藏
}
}
// 正例:至少记录日志,或按业务重新抛出/降级
async function good() {
try {
await risky();
} catch (e) {
console.error('risky 失败:', e);
throw e; // 或返回降级值
}
}坑六:串行的循环导致性能低下
// 反例:明明可以并发,却在循环里逐个 await
async function bad(urls) {
const results = [];
for (const url of urls) {
results.push(await fetch(url)); // 一个接一个,累加耗时
}
return results;
}
// 正例:并发(注意并发量,必要时配合并发池)
async function good(urls) {
return Promise.all(urls.map(url => fetch(url)));
}坑七:竞态条件导致旧结果覆盖新结果
// 反例:快速切换时,慢的旧请求后返回,覆盖了新请求结果
let latestData;
async function bad(id) {
const data = await fetchItem(id);
latestData = data; // 无法保证这是最新的那次
}
// 正例:用请求序号或 AbortController 保证只采纳最新结果
let requestId = 0;
async function good(id) {
const currentId = ++requestId;
const data = await fetchItem(id);
if (currentId === requestId) {
latestData = data; // 只有最新一次才生效
}
}异步模式最佳实践
错误处理:
并发控制:
性能优化:
代码组织:
常见异步场景
网络请求:
文件操作:
定时器:
事件处理:
总结
JavaScript 的异步编程经历了从回调函数,到 Promise,再到 async/await 的演进。每一次演进都在解决前一代方案的痛点:Promise 用链式调用和统一错误冒泡解决了回调地狱;async/await 用同步风格的语法让异步代码更易读易维护,其底层是生成器与协程。
理解事件循环、宏任务与微任务的执行顺序,是准确预测异步行为的基础。在实战中,要根据依赖关系选择串行或并行,用 Promise 的各种组合方法(all/allSettled/race/any)声明式地控制并发,用并发池限制资源占用,用防抖节流控制高频触发,用 AbortController 取消过期请求并防止竞态。
掌握这些模式与工具,配合完善的错误处理与性能优化意识,就能编写出既高性能又健壮、易维护的异步代码。
异步方案演进对比总结表
| 方案 | 出现时期 | 优点 | 缺点 | 适用场景 |
| --- | --- | --- | --- | --- |
| 回调函数 | 最早 | 简单直接,无需额外概念 | 回调地狱、错误处理繁琐、难组合 | 简单一次性异步、老代码 |
| Promise | ES2015 | 链式调用、统一错误冒泡、可组合 | then 链较长时仍显繁琐 | 需要并发组合、封装底层异步 |
| async/await | ES2017 | 同步风格、易读、易调试 | 需理解其非阻塞本质,易误用串行 | 绝大多数业务异步逻辑 |
| 生成器 + 执行器 | ES2015 | 可暂停恢复、协程能力 | 需手写执行器,已被 async 取代 | 学习原理、特殊流程控制 |
| 异步迭代器 | ES2018 | 优雅消费异步流/分页数据 | 概念较新,需 for await 支持 | 分页拉取、流式数据、逐块读取 |