美文网首页
Node.js Async/Await 不完全指南

Node.js Async/Await 不完全指南

作者: xiaodongod | 来源:发表于2019-06-19 13:36 被阅读0次

可以说 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了来一发吧!

原文地址

相关文章

  • Node.js Async/Await 不完全指南

    可以说 Node.js 7.6.0最大的新特性就是让人期待已久的async函数 . Callback 天坑 and...

  • async和await

    浅谈Async/Await用 async/await 来处理异步 async和await async:声明一个异步...

  • koa2学习笔记

    快速开始 安装 node.js 版本:v7.6 以上 npm i koa -S 运行 async / await ...

  • koa2 入坑(1)之处理get/post请求

    一、安装 要求Node.js版本高于V7.6。因为node.js 7.6版本开始完全支持async/await,不...

  • coroutine, promise, async, await

    结论:promise, async, await 是不完全的抽象,coroutine更适合异步开发,解放心智负担。...

  • koa2

    环境准备 node v8.11.1 (因为node.js v7.6.0开始完全支持async/await) 安装...

  • 从零开始的Koa实战(1) 初识Koa

    前期准备 为了更好的使用 async/await ,我们选择 7.6.0 以上的 node.js 环境,当然,我们...

  • ES8(一) —— async&await

    目录 async和普通函数的区别 await async/await处理多回调异步 async和await必须配合...

  • async

    async/await特点 async/await更加语义化,async是“异步”的简写,async functi...

  • ES6中的好东西

    1 Await/Async 前端的回调时代我没有赶上,我赶上的是await/async时代。await和async...

网友评论

      本文标题:Node.js Async/Await 不完全指南

      本文链接:https://www.haomeiwen.com/subject/rvhqqctx.html