深入理解 Async/Await:现代 JavaScript 异步编程的优雅解决方案
在现代 JavaScript 开发中,异步编程是一个无法回避的话题。从早期的回调函数到 Promise,再到 Generator 函数,JavaScript 一直在探索更优雅的异步编程解决方案。而 async/await 的出现,可以说是 JavaScript 异步编程领域的一次重大突破,它让异步代码的书写和阅读变得更加直观和简洁。
什么是 Async 函数?
Async 函数实际上是 Generator 函数的语法糖,但它在多个方面进行了重要优化,使得异步编程变得更加简单和直观。
内置执行器
与 Generator 函数需要额外的执行器(如 co 模块)不同,async 函数内置了执行器,可以像普通函数一样直接调用:
javascript
复制下载
async function fn() {
return '张三';
}
const result = fn(); // 直接调用,无需额外执行器
console.log(result); // Promise {<fulfilled>: '张三'}
更好的语义
从字面上看,async 和 await 关键字直接表达了异步操作的语义。async 表示函数内部有异步操作,await 表示需要等待一个异步操作的完成。这种直观的表达方式大大提高了代码的可读性。
更广的适用性
await 命令后面不仅可以跟 Promise 对象,还可以跟原始类型的值(数值、字符串、布尔值等),这时这些值会被自动转成立即 resolve 的 Promise 对象:
javascript
复制下载
async function f() {
const a = await 'hello'; // 等同于 await Promise.resolve('hello')
const b = await 123; // 等同于 await Promise.resolve(123)
return a + b;
}
f().then(console.log); // 'hello123'
返回值是 Promise
async 函数总是返回一个 Promise 对象,这意味着我们可以使用 then 方法链式处理异步操作的结果:
javascript
复制下载
async function fn() {
return '张三';
}
fn().then(value => {
console.log(value); // '张三'
});
Async 函数的返回值详解
async 函数的返回值行为有几种不同的情况,理解这些细节对于正确使用 async 函数至关重要。
返回非 Promise 类型的对象
当 async 函数返回一个非 Promise 类型的对象时,返回值会被包装成一个成功状态的 Promise 对象:
javascript
复制下载
async function fn() {
return '张三';
}
const result = fn();
console.log(result); // Promise {<fulfilled>: '张三'}
fn().then(value => {
console.log(value); // '张三'
});
抛出错误
当 async 函数内部抛出错误时,返回值是一个失败状态的 Promise:
javascript
复制下载
async function fn() {
throw new Error('出错了');
}
const result = fn();
console.log(result); // Promise {<rejected>: Error: 出错了}
fn().then(
value => console.log(value),
reason => console.log(reason) // Error: 出错了
);
返回 Promise 对象
当 async 函数返回一个 Promise 对象时,该 Promise 对象的状态决定了 async 函数返回的 Promise 状态:
javascript
复制下载
async function fn() {
return new Promise((resolve, reject) => {
// resolve('成功了');
reject('失败了');
});
}
const result = fn();
console.log(result); // Promise {<rejected>: '失败了'}
fn().then(
value => console.log(value),
reason => console.log(reason) // '失败了'
);
Await 表达式的深入理解
await 表达式是 async/await 的核心,它只能在 async 函数内部使用,具有以下几个重要特性。
等待 Promise 完成
await 后面通常跟一个 Promise 对象,它会暂停 async 函数的执行,等待 Promise 完成,然后返回 Promise 的成功值:
javascript
复制下载
const p = new Promise((resolve, reject) => {
resolve('成功了');
// reject('失败了');
});
async function f1() {
const result = await p;
console.log(result); // '成功了'
}
f1();
错误处理
当 await 后面的 Promise 变为拒绝状态时,await 表达式会抛出异常,需要通过 try...catch 结构来捕获:
javascript
复制下载
const p = new Promise((resolve, reject) => {
reject('失败了');
});
async function f2() {
try {
const result = await p;
console.log(result);
} catch(err) {
console.log(err); // '失败了'
}
}
f2();
等待 Thenable 对象
await 后面不仅可以跟 Promise 对象,还可以跟任何定义了 then 方法的对象(thenable 对象),await 会将其视为 Promise 对象来处理:
javascript
复制下载
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(() => {
resolve(Date.now() - startTime);
}, this.timeout);
}
}
(async () => {
const sleepTime = await new Sleep(1000);
console.log(sleepTime); // 大约 1000
})();
这种特性使得我们可以创建自定义的异步操作,只要对象实现了 then 方法,就可以与 await 一起使用。
错误处理策略
在 async 函数中,错误处理是一个需要特别注意的方面。
中断执行的问题
默认情况下,任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行:
javascript
复制下载
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
防止中断执行的策略
有时我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将 await 放在 try...catch 结构里面:
javascript
复制下载
async function f() {
try {
await Promise.reject('出错了');
} catch (e) {
// 捕获错误,但不中断执行
}
return await Promise.resolve('hello world');
}
f().then(v => console.log(v)); // 'hello world'
实际应用场景
文件读取
async/await 在处理多个顺序执行的异步操作时特别有用,比如文件读取:
// 模拟文件读取函数
function read1() {
return new Promise((resolve, reject) => {
// 模拟异步文件读取
setTimeout(() => {
resolve('文件1的内容');
}, 1000);
})
}
function read2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('文件2的内容');
}, 1000);
})
}
function read3() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('文件3的内容');
}, 1000);
})
}
async function main() {
try {
const result1 = await read1();
console.log(result1);
const result2 = await read2();
console.log(result2);
const result3 = await read3();
console.log(result3);
} catch(err) {
console.log(err);
}
}
main();
并发执行优化
虽然上面的例子展示了顺序执行异步操作,但在实际开发中,如果多个异步操作之间没有依赖关系,我们可以使用 Promise.all 来并发执行,提高效率:
javascript
复制下载
async function main() {
try {
const [result1, result2, result3] = await Promise.all([
read1(),
read2(),
read3()
]);
console.log(result1, result2, result3);
} catch(err) {
console.log(err);
}
}
Async/Await 与传统异步方案的对比
与 Promise 链的对比
使用传统的 Promise 链:
javascript
复制下载
function fetchData() {
return fetch('/api/data1')
.then(response => response.json())
.then(data1 => {
return fetch('/api/data2')
.then(response => response.json())
.then(data2 => {
return { data1, data2 };
});
});
}
使用 async/await:
javascript
复制下载
async function fetchData() {
const response1 = await fetch('/api/data1');
const data1 = await response1.json();
const response2 = await fetch('/api/data2');
const data2 = await response2.json();
return { data1, data2 };
}
可以看到,async/await 版本的代码更加直观,逻辑更加清晰。
与 Generator 函数的对比
使用 Generator 函数处理异步:
javascript
复制下载
function* fetchData() {
const response1 = yield fetch('/api/data1');
const data1 = yield response1.json();
const response2 = yield fetch('/api/data2');
const data2 = yield response2.json();
return { data1, data2 };
}
// 需要执行器
function run(generator) {
const iterator = generator();
function iterate(iteration) {
if (iteration.done) return iteration.value;
const promise = iteration.value;
return promise.then(result => iterate(iterator.next(result)));
}
return iterate(iterator.next());
}
run(fetchData);
使用 async/await:
javascript
复制下载
async function fetchData() {
const response1 = await fetch('/api/data1');
const data1 = await response1.json();
const response2 = await fetch('/api/data2');
const data2 = await response2.json();
return { data1, data2 };
}
// 直接调用
fetchData();
明显可以看出,async/await 方案更加简洁,无需额外的执行器。
最佳实践和注意事项
1. 始终处理错误
在使用 async/await 时,不要忘记错误处理。可以使用 try...catch 结构,或者使用 .catch() 方法:
javascript
复制下载
// 方式一:使用 try...catch
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('获取数据失败:', error);
throw error; // 或者返回默认值
}
}
// 方式二:使用 .catch()
fetchData().catch(error => {
console.error('获取数据失败:', error);
});
2. 避免不必要的 await
不要滥用 await,只有在需要等待异步操作完成时才使用它:
javascript
复制下载
// 不推荐
async function example() {
const a = await 1; // 不必要的 await
const b = await 2; // 不必要的 await
return a + b;
}
// 推荐
async function example() {
const a = 1;
const b = 2;
return a + b;
}
3. 合理使用并发
当多个异步操作之间没有依赖关系时,应该并发执行它们,而不是顺序执行:
// 不推荐 - 顺序执行
async function fetchSequential() {
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
return { user, posts, comments };
}
// 推荐 - 并发执行
async function fetchConcurrent() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
return { user, posts, comments };
}
总结
Async/await 是 JavaScript 异步编程的重大进步,它通过更加直观和简洁的语法,让我们能够以近乎同步的方式编写异步代码,同时保持了异步操作的非阻塞特性。
从本质上讲,async 函数是 Generator 函数的语法糖,但它通过内置执行器、更好的语义、更广的适用性和 Promise 返回值等优化,大大提升了开发体验。await 表达式则让我们能够以同步的方式编写异步逻辑,使代码更加清晰易读。