node.js中的Async Await

无论你之前是否了解过JavaScript中的async/awaitpromises,但还没有完全掌握它们,或者只是需要一个复习.

Node.js中的异步函数是直接可用的,它们在声明中使用async关键字标识。它们总是返回一个promise,即使你没有显式地将它们编写为这样。此外,await关键字目前只能在async函数内部使用 - 它不能在全局范围内使用

async函数中,你可以等待任何Promise或捕获其拒绝原因。

因此,如果你用promises实现了一些逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function handler (req, res) {
return request('https://user-handler-service')
.catch((err) => {
logger.error('Http error', err);
error.logged = true;
throw err;
})
.then((response) => Mongo.findOne({ user: response.body.user }))
.catch((err) => {
!error.logged && logger.error('Mongo error', err);
error.logged = true;
throw err;
})
.then((document) => executeLogic(req, res, document))
.catch((err) => {
!error.logged && console.error(err);
res.status(500).send();
});
}

你可以使用async/await使它看起来像同步代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function handler (req, res) {
let response;
try {
response = await request('https://user-handler-service') ;
} catch (err) {
logger.error('Http error', err);
return res.status(500).send();
}

let document;
try {
document = await Mongo.findOne({ user: response.body.user });
} catch (err) {
logger.error('Mongo error', err);
return res.status(500).send();
}

executeLogic(document, req, res);
}

目前在Node中你会收到有关未处理promise拒绝的警告,因此你不一定需要创建一个监听器。然而,建议在这种情况下使你的应用程序崩溃,因为当你不处理错误时,你的应用程序处于未知状态。这可以通过使用 --unhandled-rejections=strict CLI标志,或者实现类似这样的东西来完成:

1
2
3
4
process.on('unhandledRejection', (err) => { 
console.error(err);
process.exit(1);
})

JavaScript中的异步函数模式

在处理异步操作时,将其看作同步操作的能力在许多情况下非常方便,因为使用Promisecallback解决它们需要使用复杂的模式。

自从node@10.0.0开始,支持异步迭代器和相关的for-await-of循环。当我们迭代的实际值和迭代结束状态在迭代器方法返回时不确定时,这些都非常方便 - 主要是在处理流时。除了流之外,没有太多的构造本身实现了异步迭代器,所以我们将在另一篇文章中介绍它们。

使用自增控制重试

使用Promise实现重试逻辑相当笨拙:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function request(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(`Network error when trying to reach ${url}`);
}, 500);
});
}

function requestWithRetry(url, retryCount, currentTries = 1) {
return new Promise((resolve, reject) => {
if (currentTries <= retryCount) {
const timeout = (Math.pow(2, currentTries) - 1) * 100;
request(url)
.then(resolve)
.catch((error) => {
setTimeout(() => {
console.log('Error: ', error);
console.log(`Waiting ${timeout} ms`);
requestWithRetry(url, retryCount, currentTries + 1);
}, timeout);
});
} else {
console.log('No retries left, giving up.');
reject('No retries left, giving up.');
}
});
}

requestWithRetry('http://localhost:3000')
.then((res) => {
console.log(res)
})
.catch(err => {
console.error(err)
});

这样可以完成任务,但我们可以使用async/await重写它,使其更简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function wait (timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, timeout);
});
}

async function requestWithRetry (url) {
const MAX_RETRIES = 10;
for (let i = 0; i <= MAX_RETRIES; i++) {
try {
return await request(url);
} catch (err) {
const timeout = Math.pow(2, i);
console.log('Waiting', timeout, 'ms');
await wait(timeout);
console.log('Retrying', err.message, i);
}
}
}

中间值

与上一个示例不那么可怕,但如果你有一个情况,其中3个异步函数相互依赖,如下所示,那么你必须从几个解决方案中选择。

functionA返回一个Promise,然后functionB需要该值,functionC需要functionA和functionB的Promise的已解析值。

  • 解决方案1:.then Christmas tree
1
2
3
4
5
6
7
8
9
function executeAsyncTask () {
return functionA()
.then((valueA) => {
return functionB(valueA)
.then((valueB) => {
return functionC(valueA, valueB)
})
})
}

使用这个解决方案,我们从第三个then的周围闭包中获取valueA,并将valueB作为前一个Promise解析的值。我们不能将Christmas tree展平,因为我们会失去闭包,并且valueA对于functionC不可用。

  • 解决方案2:移至更高的作用域
1
2
3
4
5
6
7
8
9
10
11
function executeAsyncTask () {
let valueA
return functionA()
.then((v) => {
valueA = v
return functionB(valueA)
})
.then((valueB) => {
return functionC(valueA, valueB)
})
}

Christmas tree中,我们使用了一个更高的作用域来使valueA也可用。这个情况工作方式类似,但现在我们将变量valueA创建在.then-s的作用域之外,所以我们可以将第一个解析的Promise的值赋给它。

这个肯定有效,展平了.then链,并且在语义上是正确的。然而,它也为在函数中其他地方使用变量名valueA打开了新的bug的可能性。我们还需要使用两个名称 - valueAv - 来表示相同的值。

  • 解决方案3:不必要的array
1
2
3
4
5
6
7
8
9
function executeAsyncTask () {
return functionA()
.then(valueA => {
return Promise.all([valueA, functionB(valueA)])
})
.then(([valueA, valueB]) => {
return functionC(valueA, valueB)
})
}

没有其他理由要求将valueAPromise functionB一起传递到数组中以便展开树。它们可能是完全不同类型的,因此它们不属于数组的可能性很高。

  • 解决方案4:编写辅助函数
1
2
3
4
5
6
7
8
9
10
11
12
const converge = (...promises) => (...args) => {
let [head, ...tail] = promises
if (tail.length) {
return head(...args)
.then((value) => converge(...tail)(...args.concat([value])))
} else {
return head(...args)
}
}

functionA(2)
.then((valueA) => converge(functionB, functionC)(valueA))

当然,您可以编写一个辅助函数来隐藏上下文切换,但这很难阅读,并且对于不熟悉函数式编程的人来说可能不容易理解。

通过使用async/await,我们的问题就神奇地消失了:

1
2
3
4
5
async function executeAsyncTask () {
const valueA = await functionA();
const valueB = await functionB(valueA);
return function3(valueA, valueB);
}

多个并行请求与async/await

这与前面的例子类似。如果您想同时执行几个异步任务,然后在不同的位置使用它们的值,您可以轻松地使用async/await

1
2
3
4
5
6
async function executeParallelAsyncTasks () {
const [ valueA, valueB, valueC ] = await Promise.all([ functionA(), functionB(), functionC() ]);
doSomethingWith(valueA);
doSomethingElseWith(valueB);
doAnotherThingWith(valueC);
}

正如我们在前面的例子中看到的,我们要么需要将这些值移至更高的作用域,要么创建一个非语义的数组来传递这些值。

数组迭代方法

您可以使用mapfilterreduceasync函数,尽管它们的行为非常不直观。试着猜猜下面的脚本会打印什么到控制台:

map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function asyncThing (value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), 100);
});
}

async function main () {
return [1,2,3,4].map(async (value) => {
const v = await asyncThing(value);
return v * 2;
});
}

main()
.then(v => console.log(v))
.catch(err => console.error(err));
filter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function asyncThing (value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), 100);
});
}

async function main () {
return [1,2,3,4].filter(async (value) => {
const v = await asyncThing(value);
return v % 2 === 0;
});
}

main()
.then(v => console.log(v))
.catch(err => console.error(err));
reduce
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function asyncThing (value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), 100);
});
}

async function main () {
return [1,2,3,4].reduce(async (acc, value) => {
return await acc + await asyncThing(value);
}, Promise.resolve(0));
}

main()
.then(v => console.log(v))
.catch(err => console.error(err));
解决方案:
1
2
3
[ Promise { <pending> }, Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
[ 1, 2, 3, 4 ]
10

如果您记录迭代器的返回值,您将会看到我们预期的数组:[ 2, 4, 6, 8 ]。唯一的问题是每个值都被AsyncFunction包装在一个Promise中。

因此,如果您想获取您的值,您需要通过将返回的数组传递给Promise.all来解包它们:

1
2
3
4
main()
.then(v => Promise.all(v))
.then(v => console.log(v))
.catch(err => console.error(err));

最初,您将首先等待所有的promises解决,然后映射值:

1
2
3
4
5
6
7
8
function main () {
return Promise.all([1,2,3,4].map((value) => asyncThing(value)));
}

main()
.then(values => values.map((value) => value * 2))
.then(v => console.log(v))
.catch(err => console.error(err));

这看起来更简单了,不是吗?

如果您的迭代器中有一些长时间运行的同步逻辑和另一个长时间运行的异步任务,async/await版本仍然很有用。

这样,一旦您获得了第一个值,您就可以开始计算 - 您不必等待所有的Promises解决才能运行您的计算。尽管结果仍然被包装在Promises中,但这些结果比按顺序执行要快得多。

filter呢?显然有些不对劲…

嗯,您猜对了:即使返回的值是[ false, true, false, true ],它们也将被包装在promises中,而promises是真值,所以您将从原始数组中获得所有的值。不幸的是,您唯一可以做的是解决所有的值,然后再过滤它们。

Reducing很简单。请记住,您需要将初始值包装到Promise.resolve,因为返回的累加器也将被包装,并且必须等待。
缩减非常简单。但请记住,您需要将初始值包装到Promise.resolve中,因为返回的累加器也将被包装,并且必须等待它。

重写基于callbackNode.js应用程序

异步函数默认返回Promise,因此您可以重写任何基于回调的函数以使用Promises,然后等待它们的解决。您可以在Node.js中使用util.promisify函数将基于回调的函数转换为返回基于Promise的函数。

重写基于Promise的应用程序

简单的.then链可以以非常直接的方式升级,因此您可以立即转换为使用async/await

1
2
3
4
5
6
7
8
9
function asyncTask () {
return functionA()
.then((valueA) => functionB(valueA))
.then((valueB)

=> functionC(valueB))
.then((valueC) => functionD(valueC))
.catch((err) => logger.error(err))
}

将变成

1
2
3
4
5
6
7
8
9
10
async function asyncTask () {
try {
const valueA = await functionA();
const valueB = await functionB(valueA);
const valueC = await functionC(valueB);
return await functionD(valueC);
} catch (err) {
logger.error(err);
}
}

使用async/await重写Node.js应用程序

  • 如果您喜欢旧的if-else条件和for/while循环概念,
  • 如果您认为try-catch块是处理错误的正确方式,
  • 那么您将喜欢使用async/await重写您的服务。

async/await可以使几种模式的编码和阅读变得更容易,因此在某些情况下,它绝对比Promise.then()链更合适。