彻底理解 JavaScript 异步编程
JavaScript 是单线程语言,异步是它处理 I/O 的核心机制。理解异步演进历史,有助于写出更清晰的代码。
第一代:回调函数
最早的异步方案,直接简单,但嵌套深了就成了”回调地狱”:
fs.readFile('a.txt', (err, dataA) => {
if (err) throw err;
fs.readFile('b.txt', (err, dataB) => {
if (err) throw err;
fs.writeFile('result.txt', dataA + dataB, (err) => {
if (err) throw err;
console.log('完成');
});
});
});
问题显而易见:错误处理重复、嵌套层级深、逻辑难以追踪。
第二代:Promise
Promise 将异步操作封装成对象,支持链式调用:
readFile('a.txt')
.then(dataA => readFile('b.txt').then(dataB => [dataA, dataB]))
.then(([dataA, dataB]) => writeFile('result.txt', dataA + dataB))
.then(() => console.log('完成'))
.catch(err => console.error(err));
需要并行执行多个请求时,使用 Promise.all:
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts(),
]);
第三代:async / await
语法糖,让异步代码读起来像同步:
async function processFiles() {
try {
const dataA = await readFile('a.txt');
const dataB = await readFile('b.txt');
await writeFile('result.txt', dataA + dataB);
console.log('完成');
} catch (err) {
console.error(err);
}
}
三种方式对比
| 方式 | 错误处理 | 可读性 | 并行支持 |
|---|---|---|---|
| 回调 | 每个回调手动处理 | 差(嵌套深) | 困难 |
| Promise | .catch() 链 | 中 | Promise.all |
| async/await | try/catch | 好 | 需配合 Promise.all |
常见陷阱
在循环中错误使用 await
// 错误:串行执行,效率低
for (const id of ids) {
const user = await fetchUser(id);
users.push(user);
}
// 正确:并行执行
const users = await Promise.all(ids.map(id => fetchUser(id)));
忘记处理 Promise rejection
// 危险:未捕获的 rejection 会导致进程崩溃(Node.js)
fetchData().then(process);
// 安全
fetchData().then(process).catch(handleError);
// 或用 async/await
try {
const data = await fetchData();
process(data);
} catch (err) {
handleError(err);
}
async 函数永远返回 Promise
async function getAge() {
return 18;
}
// 不能直接这样用
const age = getAge(); // age 是 Promise,不是 18
console.log(age + 1); // NaN
// 正确
const age = await getAge(); // 18
总结
现代 JavaScript 项目应默认使用 async/await,在需要并行执行多个请求时配合 Promise.all。回调函数仅在对接旧接口时使用,且建议用 util.promisify(Node.js)将其包装成 Promise。
判断标准很简单:如果一个函数需要等待某件事完成,就声明为
async;如果一个值可能还没准备好,就await它。