async/await
真是个好东西。有了它,在 Node.js 中写异步代码简直如丝般顺滑,再也不需要引入第三方库来帮忙了,还能摆脱复杂的 Promise 链式调用。async/await
的应用场景有哪些?最佳实践又是什么?本文为你一一道来。
重试失败请求
在 Node 中发送 HTTP 请求,如果请求失败就重试指定的次数。用回调函数的方式大概会这么写:
const superagent = require('superagent');
const NUM_RETRIES = 3;
request('http://google.com/this-throws-an-error', function(error, res) {
console.log(error.message); // "Not Found"
});
function request(url, callback) {
_request(url, 0, callback);
}
function _request(url, retriedCount, callback) {
superagent.get(url).end(function(error, res) {
if (error) {
if (retriedCount >= NUM_RETRIES) {
return callback && callback(error);
}
return _request(url, retriedCount + 1, callback);
}
callback(res);
});
}
也不算太难,但是用到了递归,对初学者来说稍微有点绕。另外,这里还存在一个小问题:如果superagent.get().end()
这个函数调用本身抛出了同步异常时怎么办?我们可能需要在 _request()
外面再包一层try/catch
用于捕获所有异常。所有调用这个方法的地方都需要这么做,这就有点麻烦了,还容易出错。对比下用async/await
实现同样效果的代码,只需要for
循环 和 try/catch
:
const superagent = require('superagent');
const NUM_RETRIES = 3;
test();
async function test() {
let i;
for (i = 0; i < NUM_RETRIES; ++i) {
try {
await superagent.get('http://google.com/this-throws-an-error');
break;
} catch(err) {}
}
console.log(i); // 3
}
这段代码的巧妙之处在于,请求无异常就会退出for
循环,否则进入下一次循环,直到超过指定次数。没有回调,没有递归,看上去是不是清晰多了?但是要注意, await
必须在async
函数内部使用,嵌套包含也不行。下面代码中 forEach()
的回调函数不是async
函数,因此不能使用await
:
const superagent = require('superagent');
const NUM_RETRIES = 3;
test();
async function test() {
let arr = new Array(NUM_RETRIES).map(() => null);
arr.forEach(() => {
try {
// SyntaxError: Unexpected identifier. This `await` is not in an async function!
await superagent.get('http://google.com/this-throws-an-error');
} catch(err) {}
});
}
处理 MongoDB 游标
MongoDB 的 find()
返回一个 游标。游标实际上是一个带有异步 next()
函数的对象,next
用于获取查询结果中的下一个文档。如果所有结果都获取完毕,next()
就返回null
。MongoDB 游标有几个助手函数,比如 each()
、map()
和toArray()
,mongoose ODM 还增加了一个额外的eachAsync()
,不过这些都只是next()
上面的语法糖。
没有async/await
的话,手动调用next()
重试失败请求也要用到递归,跟前面的例子一样。有了async/await
,你会发现再也不用上那些助手函数了(除了toArray()
),因为只要用for
循环遍历游标就行了,简单直接!
const mongodb = require('mongodb');
test();
async function test() {
const db = await mongodb.MongoClient.connect('mongodb://localhost:27017/test');
await db.collection('Movies').drop();
await db.collection('Movies').insertMany([
{ name: 'Enter the Dragon' },
{ name: 'Ip Man' },
{ name: 'Kickboxer' }
]);
// 获取游标对象
const cursor = db.collection('Movies').find();
// 用 `next()` 和 `await` 移动游标
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
console.log(doc.name);
}
}
如果觉得还不够方便,还可以用async
迭代器(ES2018 引入的特性,Node.js 10.x 开始支持)。
const cursor = db.collection('Movies').find().map(value => ({
value,
done: !value
}));
for await (const doc of cursor) {
console.log(doc.name);
}
并发执行异步任务
上面的例子都是按顺序执行多个请求,任何时候都只有一个next()
函数在执行。如何并发执行多个异步任务?假设你要用 bcrypt同时执行多个密码哈希操作:
const bcrypt = require('bcrypt');
const NUM_SALT_ROUNDS = 8;
test();
async function test() {
const pws = ['password', 'password1', 'passw0rd'];
// `promises` 是 promise 数组, `bcrypt.hash()` 如果没指定回调函数,就会返回 promise
const promises = pws.map(pw => bcrypt.hash(pw, NUM_SALT_ROUNDS));
/**
* Prints hashed passwords, for example:
* [ '$2a$08$nUmCaLsQ9rUaGHIiQgFpAOkE2QPrn1Pyx02s4s8HC2zlh7E.o9wxC',
* '$2a$08$wdktZmCtsGrorU1mFWvJIOx3A0fbT7yJktRsRfNXa9HLGHOZ8GRjS',
* '$2a$08$VCdMy8NSwC8r9ip8eKI1QuBd9wSxPnZoZBw8b1QskK77tL2gxrUk.' ]
*/
console.log(await Promise.all(promises));
}
Promise.all()
函数接受一个 promise 数组作为参数,并返回一个 promise。传入的多个 promise 全部被解决后,返回的 promise 才被解决,同时接收到一个数组,包含每个 promise 的执行结果。由于每个 bcrypt.hash()
返回一个 promise,promises
就是个 promise 数组,await Promise.all(promises)
的结果就是每个bcrypt.hash()
调用的结果。
Promise.all()
不是并行处理多个异步函数的唯一方式,还有 Promise.race()
函数 ,也可以并发执行多个 promise,区别是它在最先完成的 promise 之后被解决。race
的字面意思就是“竞赛”,看哪个 promise 先完成。下面是Promise.race()
配合使用async/await
的例子:
/**
* Prints below:
* waited 250
* resolved to 250
* waited 500
* waited 1000
*/
test();
async function test() {
const promises = [250, 500, 1000].map(ms => wait(ms));
console.log('resolved to', await Promise.race(promises));
}
async function wait(ms) {
await new Promise(resolve => setTimeout(() => resolve(), ms));
console.log('waited', ms);
return ms;
}
需要注意的是,虽然 Promise.race()
在第一个被完成的 promise 之后就被解决,但是其余的async
函数依然会继续执行。因为 promise 目前是无法取消执行的。
总结
Async/await
被认为是 JavaScript 异步编程的终极方案。这个特性不但减少了代码量,还极大地提高了可读性。在异常处理、重试请求和执行并发任务方面有很大的优势,值得大力推广使用。
![](https://img.haomeiwen.com/i1618526/5e3cd1f88c819043.png)
网友评论