Node.js Async/Await 不完全指南

可以说 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上使用awaitBluebird 或者其它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了来一发吧!

原文地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容