可以说 Node.js 7.6.0最大的新特性就是让人期待已久的async
函数 . Callback 天坑 and promise
天坑现在都已经是过去式了。 但是, 就像Uncle Ben常说过的, 能力越大,责任越大, 而 async/await
给了你一百种又新又奇的方法搬起石头砸自己的脚。 在你写代码时仍然需要处理errors
和了解async
的本质,否则,在接下来的六个月我们免不了会抱怨 "async/await 天坑"。
这篇文章中的所有代码都是在node.js 7.6.0\测试通过的。在更早的版本是运行不了的。 Node.js 7.x Node.js的一个奇数的发行版,这就意味着它预定在2017年6月被废弃, 所以我不建议在生产环境中使用它。
Hello, World
这里是使用async/await的一个"Hello, World"示例:
async function test() {
await new Promise((resolve, reject) => setTimeout(() => resolve(), 1000));
console.log('Hello, World!');
}
test();
你可以像往常一样直接运行这段脚本,不需要任何转换编译器,大约一秒后,它会打印"Hello, World!"。
$ ~/Workspace/node-v7.6.0-linux-x64/bin/node async.js
Hello, World!
$
$ time ~/Workspace/node-v7.6.0-linux-x64/bin/node async.js
Hello, World!
real 0m1.121s
user 0m0.115s
sys 0m0.008s
$
Async 函数是完全基于promises的。你应该始终在promise上await
。 在一个非promise上使用await
不会做任何事情:
async function test() {
// Works, just doesn't do anything useful
await 5;
console.log('Hello, World!');
}
test();
你不一定要在原生Node.js promise上使用await
。Bluebird 或者其它promise库也可以。一般来说,在任何有then()
函数属性的对象上使用await
都是可以的。
async function test() {
// This object is a "thenable". It's a promise by the letter of the law,
// but not the spirit of the law.
await { then: resolve => setTimeout(() => resolve(), 1000) };
console.log('Hello, World!');
}
test();
使用await
一个重要的约束是:你必须在一个定义为async
的函数中使用await
。以下代码运行会提示语法错误:
function test() {
const p = new Promise(resolve => setTimeout(() => resolve(), 1000));
// SyntaxError: Unexpected identifier
await p;
}
test();
此外,await
不能是一个闭包嵌入在async
函数中,除非这个闭包也是一个async
函数。以下代码运行也会提示语法错误:
const assert = require('assert');
async function test() {
const p = Promise.resolve('test');
assert.doesNotThrow(function() {
// SyntaxError: Unexpected identifier
await p;
});
console.log('Hello, world!');
}
test();
另外一个需要记住的关于async
函数的细节是,async
函数返回的是promise:
async function test() {
await new Promise((resolve, reject) => setTimeout(() => resolve(), 1000));
console.log('Hello, World!');
}
// Prints "Promise { <pending> }"
console.log(test());
这意味着你可以在一个async
函数的返回结果上await
。
async function wait(ms) {
await new Promise(resolve => setTimeout(() => resolve(), ms));
}
async function test() {
// Since `wait()` is marked `async`, the return value is a promise, so
// you can `await`
await wait(1000);
console.log('Hello, World!');
}
test();
返回值和异常
Promise既可以被解决(resolve)后返回一个值,也可以因为一个错误被拒绝(reject)。Async/await可以让你使用同步的方式处理这些事情:分配被解决(resolved )后的值,或者try/catch
异常。await
的返回值就是对应的promise的返回值:
async function test() {
const res = await new Promise(resolve => {
// This promise resolves to "Hello, World!" after ~ 1sec
setTimeout(() => resolve('Hello, World!'), 1000);
});
// Prints "Hello, World!". `res` is equal to the value the promise resolved to
console.log(res);
}
test();
在async
函数中,你可以使用try/catch
来捕获promise的拒绝(rejections)。换句话说,异步的promise拒绝(rejections)表现的像同步的errors
:
async function test() {
try {
await new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Woops!')), 1000);
});
} catch (error) {
// Prints "Caught Woops!"
console.log('Caught', error.message);
}
}
test();
使用try/catch
作为一种错误的处理机制是很有用的,它使得你使用一种语法来同时处理同步和异步的错误。在回调部分中,你通常不得不使用try/catch
包裹你的异步调用,处理错误回调参数时也是如此。
function bad() {
throw new Error('bad');
}
function bad2() {
return new Promise(() => { throw new Error('bad2'); });
}
async function test() {
try {
await bad();
} catch (error) {
console.log('caught', error.message);
}
try {
await bad2();
} catch (error) {
console.log('caught', error.message);
}
}
test();
循环和条件判断
async/await
的头号爆炸属性就是你可以在写异步代码时使用if
判断,for
循环,以及其他那些你曾经发誓不会在回调中使用的同步结构。有了async/await
你也不再需要任何流程控制库,只需要简单的使用条件判断和循环即可。这里是一个使用for
循环的例子:
function wait(ms) {
return new Promise(resolve => setTimeout(() => resolve(), ms));
}
async function test() {
for (let i = 0; i < 10; ++i) {
await wait(1000);
// Prints out "Hello, World!" once per second and then exits
console.log('Hello, World!');
}
}
test();
另一个使用if
判断的例子:
function wait(ms) {
return new Promise(resolve => setTimeout(() => resolve(), ms));
}
async function test() {
for (let i = 0; i < 10; ++i) {
if (i < 5) {
await wait(1000);
}
// Prints out "Hello, World!" once per second 5 times, then prints it 5 times immediately
console.log('Hello, World!');
}
}
test();
记住它是异步的(Asynchronous)
我曾经问过的一个俏皮的JavaScript面试题就是下面这段代码会打印什么?
for (var i = 0; i < 5; ++i) {
// Actually prints out "5" 5 times.
// But if you use `let` above, it'll print out 0-4
setTimeout(() => console.log(i), 0);
}
// This will print *before* the 5's
console.log('end');
异步编程是很复杂的,而async/await
让编写异步代码更简单却又不会改变它的本质。仅仅因为异步函数看起来是同步的并不意味着它们就是同步的:
function wait(ms) {
return new Promise(resolve => setTimeout(() => resolve(), ms));
}
async function test(ms) {
for (let i = 0; i < 5; ++i) {
await wait(ms);
console.log(ms * (i + 1));
}
}
// These two function calls will actually run in parallel
test(70);
test(130);
// Output
70
130
140
210
260
280
350
390
520
650
错误处理
记住你仅仅只能在 async
函数中使用 await
,而async
函数返回promises。这就意味着你的代码有些地方不得不进行错误处理。Async/await
提供了一个强大的机制可以让你聚合这些错误:async
函数中的所有错误,不管是同步的还是异步的,都会向上冒泡成一个promise 拒绝(rejection)。但是,这个错误得由你自己来处理。这里有一篇很好的讲述如何使用 async/await
来处理Promise拒绝的文章。
假设你想在Express中使用async/await
,最简单的方法就是在Express最基础的例子中使用异步函数:
const express = require('express');
const app = express();
app.get('/', handler);
app.listen(3000);
async function handler(req, res) {
// Will wait approximately 1 second before sending the result
await wait(1000);
res.send('Hello, world');
}
function wait(ms) {
return new Promise(resolve => setTimeout(() => resolve(), ms));
}
完事了,对吗? 错。如果你在在handler函数中抛出一个异常会发生什么?
const express = require('express');
const app = express();
app.get('/', handler);
app.listen(3000);
async function handler(req, res) {
throw new Error('Hang!');
}
function wait(ms) {
return new Promise(resolve => setTimeout(() => resolve(), ms));
}
Express 将会被永远挂起,服务器也不会崩溃,唯一的错误提示就是一个未处理的promise拒绝警告。
$ node async.js
(node:17661) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Hang!
(node:17661) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
由于await
将promise拒绝视为异常,除非你使用try/ catch包裹await
,否则这个拒绝(rejection)将导致整个函数停止执行。
async function handler(req, res) {
await new Promise((resolve, reject) => reject(new Error('Hang!')));
res.send('Hello, World!');
}
本文中最重要的一点是,异步函数返回一个promise。Async/await让你可以使用循环、条件判断和try/catch来构建复杂的异步逻辑,并且它最后会把这些逻辑打包成一个promise。如果你看到async/await代码中没有包含任何.catch() 调用,那很可能是这段代码忽略了一些错误情况。这里有一个更好的在Express使用异步函数的例子:
const express = require('express');
const app = express();
app.get('/', safeHandler(handler));
app.listen(3000);
function safeHandler(handler) {
return function(req, res) {
handler(req, res).catch(error => res.status(500).send(error.message));
};
}
async function handler(req, res) {
await new Promise((resolve, reject) => reject(new Error('Hang!')));
res.send('Hello, World!');
}
safeHandler
函数在 异步handler
函数返回的promise上链式调用了.catch()
。这样保证了你的服务器会返回一个HTTP响应,即使handler
抛出了错误。如果在每个请求控制器上调用safeHandler
显得很冗余,也还有很多替代的方案,比如observables 或者 ramda。
Async/Await 对比 Co/Yield
co库使用 ES6 generators 实现了和async/await类似的功能。比如,这里是如何使用co/yield实现safeHandler
的示例代码:
const co = require('co');
const express = require('express');
const app = express();
app.get('/', safeHandler(handler));
app.listen(3000);
function safeHandler(handler) {
return function(req, res) {
handler(req, res).catch(error => res.status(500).send(error.message));
};
}
function handler(req, res) {
return co(function*() {
yield new Promise((resolve, reject) => reject(new Error('Hang!')));
res.send('Hello, World!');
});
}
实际上,你可以把本文的所有案例中的async function(params) {}
替换成function(params) { return co(function*() {}) }
, await
替换成yield
,程序仍然可以运行。
co可以 Node.js 4.x and 6.x很好的运行而不需要任何的转换编译。 EOL of 4.x and 6.x分别在2018和2019, 这些发行版比 Node.js 7.x更稳定。在 Node.js 8 发行之前(预计April 2017) ,还没有一个LTS版本可以无需转换编译器就能支持async/await的。Co还享有更好的浏览器支持,而且我所知的任何 async/await 转换编译器,底层也是使用的generators。
Async/await 有很多优势,最显著的就是可读的堆栈跟踪。让我们对比一下在Express中使用co和使用async/await的堆栈跟踪:
function handler(req, res) {
return co(function*() {
yield new Promise((resolve, reject) => reject(new Error('Hang!')));
res.send('Hello, World!');
});
}
// --- versus ---
async function handler(req, res) {
await new Promise((resolve, reject) => reject(new Error('Hang!')));
res.send('Hello, World!');
}
Async:
$ node async.js
Error: Hang!
at Promise (/home/val/async.js:16:49)
at handler (/home/val/async.js:16:9)
at /home/val/async.js:11:5
at Layer.handle [as handle_request] (/home/val/node_modules/express/lib/router/layer.js:95:5)
at next (/home/val/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/home/val/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/home/val/node_modules/express/lib/router/layer.js:95:5)
at /home/val/node_modules/express/lib/router/index.js:281:22
at Function.process_params (/home/val/node_modules/express/lib/router/index.js:335:12)
at next (/home/val/node_modules/express/lib/router/index.js:275:10)
Co:
$ node async.js
Error: Hang!
at Promise (/home/val/async.js:18:51)
at /home/val/async.js:18:11
at Generator.next (<anonymous>)
at onFulfilled (/home/val/node_modules/co/index.js:65:19)
at /home/val/node_modules/co/index.js:54:5
at co (/home/val/node_modules/co/index.js:50:10)
at handler (/home/val/async.js:17:10)
at /home/val/async.js:12:5
at Layer.handle [as handle_request] (/home/val/node_modules/express/lib/router/layer.js:95:5)
at next (/home/val/node_modules/express/lib/router/route.js:137:13)
因此async/await有着更好的堆栈跟踪,而且可以让你使用你所熟悉的内嵌循环和条件判断来构建promise,所以赶快下载Node.js 7.6了来一发吧!
网友评论