async/await
是 JavaScript 中用于处理异步操作的一种语法糖,它使得异步代码的编写、阅读和维护变得更加容易和直观。async
和 await
关键字是在 ES2017(ES8)中引入的,旨在简化基于 Promise 的异步操作。
1 async
async
是一个函数声明的前缀,用于指定一个函数是异步的(promise.then等回调)。这意味着函数内部可能包含异步操作,如网络请求、文件读取等。- 当你将一个函数声明为
async
时,该函数会自动返回一个 Promise。如果函数正常结束(即没有显式返回 Promise 或其他值),它将返回一个解析为undefined
的 Promise。如果函数通过return
语句返回了一个值,那么返回的 Promise 将被解析为该值。如果函数内部抛出了异常,返回的 Promise 将被拒绝(rejected),异常值作为拒绝的原因。
当然可以,以下是分别举例说明这三种情况的代码:
1. 函数正常结束,没有显式返回 Promise 或其他值
async function asyncFunctionWithoutReturn() {
// 函数体内执行一些操作,但不显式返回任何值
console.log('函数执行了,但没有返回值');
// 由于没有显式返回,所以函数将隐式返回一个解析为 undefined 的 Promise
}
let promise = asyncFunctionWithoutReturn()
promise.then(result => {
console.log(result); // 输出:undefined
}).catch(error => {
console.error(error); // 这里不会被调用,因为没有抛出异常
});
2. 函数通过 return 语句返回了一个值
async function asyncFunctionWithReturnValue() {
// 函数体内执行一些操作,并通过 return 语句返回一个值
console.log('函数执行了,并返回了一个值');
return 'Hello, async!';
// 由于有显式返回,所以函数将返回一个解析为 'Hello, async!' 的 Promise
}
let promise = asyncFunctionWithReturnValue()
promise.then(result => {
console.log(result); // 输出:Hello, async!
}).catch(error => {
console.error(error); // 这里不会被调用,因为没有抛出异常
});
3. 函数内部抛出了异常
async function asyncFunctionThrowsError() {
// 函数体内执行一些操作,并抛出一个异常
console.log('函数执行中...');
throw new Error('出错了!');
// 由于抛出了异常,所以函数将返回一个被拒绝的 Promise,异常值作为拒绝的原因
}
let promise = asyncFunctionThrowsError()
promise.then(result => {
// 这里不会被调用,因为 Promise 被拒绝了
console.log(result);
}).catch(error => {
console.error(error.message); // 输出:Error: 出错了!
// 捕获到异常,并可以在这里处理它
});
在这三个例子中,我们分别展示了当一个函数被声明为 async
时,它如何根据函数体内的不同情况自动返回一个 Promise。第一个例子展示了没有显式返回任何值时的情况,第二个例子展示了通过 return
语句返回一个值的情况,第三个例子展示了函数内部抛出异常时的情况。这些例子清楚地说明了 async
函数如何处理其返回值和异常。
2 await
await
关键字是 JavaScript 中处理异步操作的一个非常强大的工具,但它确实有一些限制和使用场景:
-
只能在
async
函数内部使用:await
只能在被async
关键字声明的函数或方法内部使用。这意味着你不能在普通的同步函数或全局作用域中使用await
。 -
等待 Promise 完成:
await
会暂停async
函数的执行,直到它等待的 Promise 被解决(fulfilled)或拒绝(rejected)。这使得你可以以类似于同步代码的方式来编写异步逻辑。 -
返回 Promise 解决的结果:当
await
等待的 Promise 被解决时,await
表达式会返回 Promise 解决的值。这个值可以被赋给变量,或者用于进一步的计算。 -
异常处理:如果
await
等待的 Promise 被拒绝,那么await
表达式会抛出一个异常。这个异常可以被async
函数内部的try...catch
语句捕获,就像处理同步代码中的异常一样。 -
提升代码可读性:使用
async/await
可以使异步代码更加清晰和易于理解,因为它允许你以更接近同步代码的方式来编写和执行异步逻辑。 -
不阻塞主线程:尽管
await
会暂停async
函数的执行,但它不会阻塞整个 JavaScript 运行时或主线程。JavaScript 运行时可以继续执行其他任务,如事件处理、定时器回调等,直到等待的 Promise 完成。
在 JavaScript 中,await
关键字用于等待一个 Promise
完成,并且它只能在 async
函数内部使用。await
的行为取决于它右侧的表达式:
-
如果表达式是
Promise
对象:await
会暂停async
函数的执行,直到该Promise
被解决(fulfilled)或拒绝(rejected)。- 如果
Promise
被成功解决,await
会返回解决的值。
- 如果Promise
被拒绝,await
会抛出一个异常,这个异常可以通过try...catch
结构来捕获和处理。
-
如果表达式不是
Promise
对象:await
会立即返回该表达式的值,而不会等待任何异步操作完成。这是因为非Promise
类型的值被视为已经解决的Promise
(即其值已经可用)。
3 async与await结合实践
当然,下面是一个典型的回调地狱(Callback Hell)的例子,这个例子通常出现在处理多个异步操作并且每个操作的结果都是下一个操作所需的输入时。我们将使用Node.js的fs
模块来模拟文件读取操作,尽管在Node.js v10及更高版本中,推荐使用fs.promises
API或util.promisify
来避免回调地狱。
回调地狱的例子
假设我们需要按顺序读取三个文件,并将它们的内容拼接起来。使用传统的回调方式,代码可能会像这样:
const fs = require('fs');
fs.readFile('file1.txt', 'utf8', (err, data1) => {
if (err) throw err;
fs.readFile('file2.txt', 'utf8', (err, data2) => {
if (err) throw err;
fs.readFile('file3.txt', 'utf8', (err, data3) => {
if (err) throw err;
console.log(data1 + data2 + data3);
});
});
});
为了解决这个问题,我们可以使用async/await
与fs.promises
API(或在较旧版本的Node.js中使用util.promisify
转换的fs.readFile
)。下面是使用fs.promises
API的示例:
const fs = require('fs').promises;
async function readFileConcatenate() {
try {
const data1 = await fs.readFile('file1.txt', 'utf8');
const data2 = await fs.readFile('file2.txt', 'utf8');
const data3 = await fs.readFile('file3.txt', 'utf8');
console.log(data1 + data2 + data3);
} catch (error) {
console.error('Error reading file:', error);
}
}
// 调用函数
readFileConcatenate();
在这个async
函数中,我们使用了await
来等待每个fs.readFile
调用的结果。由于await
只能在async
函数内部使用,因此我们将文件读取逻辑封装在了一个名为readFileConcatenate
的async
函数中。这种方式使得代码更加清晰和易于维护,同时避免了回调地狱的问题。
如果你正在使用Node.js的较旧版本,并且fs
模块没有内置的promises
API,你可以使用util.promisify
来转换fs.readFile
:
const fs = require('fs');
const util = require('util');
// 转换fs.readFile为返回Promise的函数
const readFile = util.promisify(fs.readFile);
async function readFileConcatenate() {
try {
const data1 = await readFile('file1.txt', 'utf8');
const data2 = await readFile('file2.txt', 'utf8');
const data3 = await readFile('file3.txt', 'utf8');
console.log(data1 + data2 + data3);
} catch (error) {
console.error('Error reading file:', error);
}
}
// 调用函数
readFileConcatenate();
这种方式同样有效,并且可以在不支持fs.promises
API的Node.js版本中使用。
要使用async
和await
来确保fun1
、fun2
、fun3
这三个方法按顺序调用,并且每个方法内部都执行一个异步的AJAX请求,你可以首先确保这三个方法都返回Promise。然后,在另一个方法中,你可以使用await
来等待每个方法完成后再继续执行下一个。
以下是一个简单的示例,展示了如何实现这一点:
// 假设我们使用fetch API来模拟AJAX请求(你也可以使用XMLHttpRequest或其他库)
// fun1 方法,返回一个Promise
function fun1() {
return new Promise((resolve, reject) => {
// 模拟异步请求
setTimeout(() => {
console.log('fun1 执行完毕');
resolve('fun1的结果');
}, 1000); // 假设请求耗时1秒
});
}
// fun2 方法,同样返回一个Promise
function fun2() {
return new Promise((resolve, reject) => {
// 模拟异步请求
setTimeout(() => {
console.log('fun2 执行完毕');
resolve('fun2的结果');
}, 1000); // 假设请求耗时1秒
});
}
// fun3 方法,也返回一个Promise
function fun3() {
return new Promise((resolve, reject) => {
// 模拟异步请求
setTimeout(() => {
console.log('fun3 执行完毕');
resolve('fun3的结果');
}, 1000); // 假设请求耗时1秒
});
}
// 调用这三个方法的函数,使用async和await来确保顺序执行
async function executeFunctionsInOrder() {
try {
const result1 = await fun1(); // 等待fun1完成
console.log('fun1的返回结果:', result1);
const result2 = await fun2(); // 等待fun2完成
console.log('fun2的返回结果:', result2);
const result3 = await fun3(); // 等待fun3完成
console.log('fun3的返回结果:', result3);
} catch (error) {
// 如果有任何一个函数出错,这里会捕获到错误
console.error('执行过程中发生错误:', error);
}
}
// 调用函数
executeFunctionsInOrder();
在这个例子中,fun1
、fun2
和fun3
都使用了setTimeout
来模拟异步操作(例如AJAX请求)。每个函数都返回一个Promise,该Promise在异步操作完成后被解决(resolve)。在executeFunctionsInOrder
函数中,我们使用了async
关键字来标记该函数为异步函数,这样我们就可以在函数体内使用await
来等待每个异步操作的完成。注意,await
只能用在async
函数内部。
运行这段代码,你会看到控制台按顺序输出了fun1
、fun2
和fun3
的执行结果,每个之间大约间隔1秒(由setTimeout
设置)。