async/await 是 ES2017 引入的新语法,它基于 Promise 实现,使异步代码的编写和阅读更加直观。简单来说:
- async 函数是一种特殊的函数,它的返回值总是一个 Promise
- await 关键字只能在 async 函数内部使用,它可以暂停函数的执行,等待一个 Promise 被解决(resolved)或拒绝(rejected),然后继续执行
为什么需要 async/await
让我们通过一个简单的例子来对比一下传统的 Promise 链式调用和 async/await 的区别。
假设我们有一个需求:从服务器获取用户信息,然后根据用户信息获取用户的订单列表,最后根据订单列表获取订单详情。
使用 Promise 链式调用的代码可能长这样:
fetchUser()
.then(user => {
return fetchOrders(user.id);
})
.then(orders => {
return fetchOrderDetails(orders[0].id);
})
.then(details => {
console.log(details);
})
.catch(error => {
console.error(error);
});
而使用 async/await 的代码则是这样的:
async function getUserOrderDetails() {
try {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
const details = await fetchOrderDetails(orders[0].id);
console.log(details);
} catch (error) {
console.error(error);
}
}
可以看到,使用 async/await 的代码更加线性,更接近我们编写同步代码的思维方式,大大提高了代码的可读性。
async/await 的基本用法
async 函数
async 函数的定义非常简单,只需要在 function 关键字前面加上 async 即可:
async function fetchData() {
}
async 函数的返回值总是一个 Promise,无论函数内部是否显式返回一个 Promise。例如:
async function greet() {
return 'Hello, world!';
}
// 等价于
function greet() {
return Promise.resolve('Hello, world!');
}
await 关键字
await 关键字只能在 async 函数内部使用,它的作用是暂停函数的执行,等待一个 Promise 被解决。例如:
async function fetchData() {
const response = await fetch('');
const data = await response.json();
return data;
}
在这个例子中,await fetch ('') 会暂停函数的执行,直到 fetch 请求完成并返回响应。然后,await response.json () 会再次暂停函数的执行,直到 JSON 数据解析完成。
需要注意的是,await 关键字只能用于 Promise。如果 await 后面跟着的不是一个 Promise,JavaScript 会自动将其包装成一个 resolved 的 Promise。例如:
async function test() {
const value = await 42;
console.log(value); // 输出42
}
这里的 42 被自动包装成了 Promise.resolve (42)。
错误处理
在 async/await 中,我们可以使用传统的 try/catch 语句来处理异步操作中可能出现的错误。例如:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
throw error;
}
}
当 await 的 Promise 被 rejected 时,catch 块会捕获到这个错误。这使得错误处理更加直观,就像处理同步代码中的错误一样。
async/await 的高级用法
并行执行多个异步操作
在某些情况下,我们可能有多个相互独立的异步操作,它们之间没有依赖关系,可以并行执行以提高效率。这时,我们可以使用 Promise.all 结合 async/await 来实现:
async function fetchMultipleData() {
const [user, products, settings] = await Promise.all([
fetchUser(),
fetchProducts(),
fetchSettings()
]);
return {
user,
products,
settings
};
}
在这个例子中,fetchUser ()、fetchProducts () 和 fetchSettings () 会同时开始执行,Promise.all 会等待所有 Promise 都被 resolved 后,才会继续执行后续代码。这样可以显著提高程序的性能。
在循环中使用 async/await
在循环中使用 async/await 需要特别注意,不同类型的循环可能会有不同的行为。
for 循环
在 for 循环中使用 async/await 会按照顺序依次执行每个异步操作:
async function processItems(items) {
for (const item of items) {
await processItem(item);
}
console.log('All items processed');
}
在这个例子中,processItem (item) 会依次执行,只有当前一个 item 处理完成后,才会处理下一个 item。
map 方法
如果使用数组的 map 方法结合 async/await,情况会有所不同:
async function processItems(items) {
const results = items.map(item => processItem(item));
await Promise.all(results);
console.log('All items processed');
}
在这个例子中,map 方法会立即为每个 item 创建一个 Promise,这些 Promise 会并行执行。然后我们使用 Promise.all 等待所有 Promise 都完成。
forEach❌
在 forEach 中使用 await 是一个常见的误区,让我们看一个例子:
async function processItems(items) {
items.forEach(async item => {
await processItem(item);
});
console.log('All items processed');
}
这段代码会有什么问题呢?答案是:console.log ('All items processed') 会在所有 item 处理完成之前就执行!
这是因为 forEach内部使用普通for循环遍历数组,它每次迭代调用传入的回调函数,整个遍历过程是同步的,并不会等待异步操作,因此forEach 方法并不支持 async/await。awite确实会暂停当前回调函数的执行,但 forEach 本身不会关心这个暂停,它会立即继续执行下一次迭代,这样所有回调函数会被依次启动,形成并行执行的效果。
要解决这个问题,我们应该使用支持 async/await 的循环结构,如 如果代码需要顺序执行,必须用for...of,
async function processItems(items) {
for (const item of items) {
await processItem(item);
}
console.log('All items processed');
}
如果允许并行执行可以使用 Promise.all 和 map 方法:
async function processItems(items) {
await Promise.all(items.map(async item => {
await processItem(item);
}));
console.log('All items processed');
}
这两种方法的本质都是利用了迭代器,不过侧重点有所不同。
for...of 循环直接利用了迭代器的顺序性,每次只处理一个元素。当遇到 await 时,它会暂停整个循环的执行,直到 Promise 被解决,确保每个异步操作按顺序完成。
而 Promise.all 结合 map 方法则是并行启动所有异步操作,再统一等待所有结果。map 方法会遍历数组并为每个元素创建一个 Promise,这些 Promise 会并行执行。Promise.all 则负责收集所有 Promise 的结果,并在所有 Promise 都解决后才继续执行后续代码。
处理多个 Promise 的竞争
有时候,我们可能需要同时发起多个请求,但只关心第一个完成的结果。这时可以使用 Promise.race 结合 async/await 来实现:
async function fetchData() {
const fastResponse = await Promise.race([
fetchFromCache(),
fetchFromNetwork()
]);
return fastResponse;
}
在这个例子中,fetchFromCache () 和 fetchFromNetwork () 会同时发起请求,Promise.race 会返回第一个完成的 Promise 的结果。
async/await 的底层原理
基于 Promise 实现
async/await 实际上是 Promise 的语法糖,它并没有引入新的语言特性,而是在 Promise 的基础上提供了更优雅的写法。
例如,下面的 async/await 代码:
async function fetchData() {
try {
const response = await fetch('');
const data = await response.json();
return data;
} catch (error) {
console.error(error);
}
}
可以转换为等价的 Promise 代码:
function fetchData() {
return fetch('')
.then(response => {
return response.json();
})
.then(data => {
return data;
})
.catch(error => {
console.error(error);
});
}
生成器 (Generator) 与自动执行器
async/await 的底层实现还涉及到生成器 (Generator) 和自动执行器的概念。
生成器是一种特殊的函数,可以暂停执行并在稍后恢复。生成器函数使用 function * 语法定义,使用 yield 关键字暂停执行。这个在上一篇文章中说过了
下面是一个简单的生成器函数示例:
function* generatorFunction() {
console.log('Step 1');
yield 1;
console.log('Step 2');
yield 2;
console.log('Step 3');
return 3;
}
const generator = generatorFunction();
console.log(generator.next()); // 输出 { value: 1, done: false }
console.log(generator.next()); // 输出 { value: 2, done: false }
console.log(generator.next()); // 输出 { value: 3, done: true }
async/await 的底层实现本质上是一个自动执行器,它会自动执行生成器函数,并处理 yield 出来的 Promise,直到生成器函数完成。
下面是一个简化的 async/await 自动执行器实现:
function run(genFn) {
const gen = genFn();
function step(key, arg) {
let result;
try {
result = gen[key](arg);
} catch (error) {
return Promise.reject(error);
}
const { value, done } = result;
if (done) {
return Promise.resolve(value);
} else {
return Promise.resolve(value).then(
val => step('next', val),
err => step('throw', err)
);
}
}
return step('next');
}
通过这种方式,JavaScript 引擎可以将 async/await 代码转换为基于生成器和 Promise 的实现,从而实现异步代码的同步化写法。
async/await 的常见应用场景
API 请求
async/await 最常见的应用场景之一就是处理 API 请求。例如:
async function fetchUserProfile(userId) {
try {
const userResponse = await fetch(`https://api.example.com/users/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`https://api.example.com/posts?userId=${userId}`);
const posts = await postsResponse.json();
return {
user,
posts
};
} catch (error) {
console.error('Error fetching user profile:', error);
throw error;
}
}
文件操作
在 Node.js 环境中,async/await 可以简化文件操作的代码:
const fs = require('fs').promises;
async function readAndProcessFile(filePath) {
try {
const data = await fs.readFile(filePath, 'utf8');
const processedData = data.toUpperCase();
await fs.writeFile(filePath + '.processed', processedData);
console.log('File processed successfully');
} catch (error) {
console.error('Error processing file:', error);
}
}
数据库操作
在数据库操作中,async/await 可以让代码更加清晰,不过不常用,且局限性太大,这里不举代码示例了。
总结
回顾处理回调地狱的演进历程:从最初的回调函数嵌套,到 Promise 的链式调用,再到如今 async/await 的同步化写法,每一次进步都在提升开发体验和代码质量。而理解这些技术的底层原理和适用场景,则是我们在实际开发中做出正确选择的关键。
希望本文能够帮助你更好地掌握 async/await 这一强大工具,在面对复杂异步操作时游刃有余。当然如果文章中有错误的地方,请你一定要指出来,我会好好修正的。