共计 2964 个字符,预计需要花费 8 分钟才能阅读完成。
异步编程的进化史
在 JavaScript 中,异步编程一直是个痛点。早期我们使用回调函数来处理异步操作,但很快就遇到了所谓的 ” 回调地狱 ”——嵌套的回调函数让代码难以阅读和维护。

// 回调地狱示例
fs.readFile('file1.txt', (err, data1) => {if (err) throw err;
fs.readFile('file2.txt', (err, data2) => {if (err) throw err;
fs.writeFile('output.txt', data1 + data2, (err) => {if (err) throw err;
console.log('完成!');
});
});
});
为了解决这个问题,Promise 应运而生,它通过链式调用让代码更清晰:
// Promise 示例
readFilePromise('file1.txt')
.then(data1 => readFilePromise('file2.txt')
.then(data2 => writeFilePromise('output.txt', data1 + data2))
.then(() => console.log('完成!'))
.catch(err => console.error(err));
但 Promise 仍然不够直观,特别是在处理复杂逻辑时。这就是 async/await 出现的背景。
async/await 的工作原理
async/await 实际上是 Generator 函数和 Promise 的语法糖。当 V8 引擎遇到 async 函数时,会将其转换为 Generator 函数 + 自动执行器的形式。
简单来说,async 函数:
- 总是返回一个 Promise
- await 表达式会暂停函数的执行
- 等到 Promise 解决后恢复执行
- 如果 Promise 被拒绝,会抛出异常
代码对比:Promise vs async/await
让我们看一个实际例子,比较 Promise 和 async/await 的实现方式。
Promise 版本:
/**
* 获取用户数据并保存
* @returns {Promise<void>}
*/
function getUserData() {return fetchUser()
.then(user => fetchProfile(user.id))
.then(profile => saveProfile(profile))
.catch(err => {console.error('操作失败:', err);
throw err;
});
}
async/await 版本:
/**
* 获取用户数据并保存
* @returns {Promise<void>}
*/
async function getUserData() {
try {const user = await fetchUser();
const profile = await fetchProfile(user.id);
await saveProfile(profile);
} catch (err) {console.error('操作失败:', err);
throw err;
}
}
可以看到,async/await 版本更接近同步代码的写法,可读性大大提高。
高级技巧
1. 使用 Promise.all 优化并行操作
当多个异步操作没有依赖关系时,可以使用 Promise.all 来并行执行:
async function loadData() {
// 顺序执行 - 慢
const user = await fetchUser();
const posts = await fetchPosts();
// 并行执行 - 快
const [user, posts] = await Promise.all([fetchUser(),
fetchPosts()]);
}
2. 立即调用的 async 函数表达式 (IIFE)
在模块顶层不能直接使用 await,但可以用 IIFE 来解决:
(async () => {const data = await fetchData();
console.log(data);
})();
3. 在 Class 中使用 async 方法
在类中定义 async 方法时要注意 this 绑定:
class ApiClient {async getData() {
// 这里的 this 会被正确保留
const response = await fetch(this.url);
return response.json();}
}
常见陷阱与解决方案
1. 循环中的 await
在循环中使用 await 会导致性能问题,因为每次迭代都会等待:
// 错误做法 - 顺序执行
for (const id of ids) {await processItem(id); // 慢!
}
// 正确做法 - 并行执行
await Promise.all(ids.map(id => processItem(id)));
2. 未处理的 Promise 拒绝
忘记处理 async 函数中的错误会导致未处理的 Promise 拒绝:
// 危险!
async function riskyOperation() {await mightFail();
}
// 安全做法
async function safeOperation() {
try {await mightFail();
} catch (err) {console.error('操作失败', err);
}
}
3. 上下文丢失问题
将 async 方法作为回调传递时容易丢失 this:
class Example {async handleClick() {// 这里的 this 可能不正确}
setup() {
// 错误 - this 丢失
button.addEventListener('click', this.handleClick);
// 正确 - 绑定 this
button.addEventListener('click', this.handleClick.bind(this));
}
}
性能考量
不同 Node.js 版本对 async/await 的优化程度不同:
- Node.js 8+: 基本支持,但性能一般
- Node.js 10+: 显著优化
- Node.js 12+: 接近手写 Promise 的性能
思考题
如何用 async 函数实现可取消的异步任务?这里提供一个思路:
/**
* 可取消的异步任务
* @param {AbortSignal} signal - 取消信号
* @returns {Promise<string>}
*/
async function cancellableTask(signal) {
// 检查是否已取消
if (signal.aborted) {throw new DOMException('Aborted', 'AbortError');
}
// 设置取消回调
return new Promise((resolve, reject) => {signal.addEventListener('abort', () => {reject(new DOMException('Aborted', 'AbortError'));
});
// 模拟异步任务
setTimeout(() => resolve('完成!'), 1000);
});
}
总结
async/await 让异步代码更易读、更易维护,但也要注意它的陷阱和最佳实践。掌握这些技巧后,你就能写出既美观又高效的异步 JavaScript 代码了。
对于更复杂的场景,可以考虑结合 RxJS 等响应式编程库,或者使用 TypeScript 来获得更好的类型安全。
