理解 Javascript 的 async await

heavylion 发布于2年前
0 条问题
 

async / await 并没有作为 ES2016 的一部分, 但这不意味着 Javascript 不会加入 这一语法特性。就在本文撰写的此刻,它正处于 Stage 3 的阶段, 并处于活跃更新状态. 这个功能 在 Edge 里已经可用 , 并且 如果在更多浏览器中被实现则进入 Statge 4 —— 可以说,下个版本该功能已经在路上了 (参考: TC39 流程) .

我们听说过这个特性已经有段时间了,但是并没有真正深入的探索它到底是怎样工作的。本文会帮助你理解这方面的内容,但此之前需要你对 promise 和 generator 已经有所了解。

使用 Promise

假设我们有如下代码,我们将一个 HTTP 请求封装在一个 Promise 里,若请求成功则将 body 成功返回,否则将 err reject 出来。这个请求每次会拉取本博客里的随机一篇文章的 HTML 内容。

var request = require('request');

function getRandomPonyFooArticle () {
  return new Promise((resolve, reject) => {
    request('https://ponyfoo.com/articles/random', (err, res, body) => {
      if (err) {
        reject(err); return;
      }
      resolve(body);
    });
  });
}

典型的使用上面 promise 的方法如下代码所示。我们构造了一个 promise 链,将 HTML 页面的一部分子集 DOM 结构转换为对应的 markdown 文档,并最终以适用于终端的方式用 console.log 打印出来。记住最好给你的 promise 都加上 .catch 错误处理。

var hget = require('hget');
var marked = require('marked');
var Term = require('marked-terminal');

printRandomArticle();

function printRandomArticle () {
  getRandomPonyFooArticle()
    .then(html => hget(html, {
      markdown: true,
      root: 'main',
      ignore: '.at-subscribe,.mm-comments,.de-sidebar'
    }))
    .then(md => marked(md, {
      renderer: new Term()
    }))
    .then(txt => console.log(txt))
    .catch(reason => console.error(reason));
}

以上代码运行起来的效果如下截图所示。

理解 Javascript 的 async await

运行截图

从代码可读性的角度来看,这段代码比使用回调更好更更有序。

使用 Generator

我们 之前已经了解怎么使用 generator 通过一种伪"同步"的方式构建可用的 html . 虽然当时的代码某种程度上可以说是同步的代码,但是需要包裹很多代码结构,而且 generator 也许并不是最直接的能够达成我们目的方式,所以我们可能还是要依靠 Promise.

function getRandomPonyFooArticle (gen) {
  var g = gen();
  request('https://ponyfoo.com/articles/random', (err, res, body) => {
    if (err) {
      g.throw(err); return;
    }
    g.next(body);
  });
}

getRandomPonyFooArticle(function* printRandomArticle () {
  var html = yield;
  var md = hget(html, {
    markdown: true,
    root: 'main',
    ignore: '.at-subscribe,.mm-comments,.de-sidebar'
  });
  var txt = marked(md, {
    renderer: new Term()
  });
  console.log(txt);
});

记住你需要在 yield 外面加上 try / catch 来捕获之前 promise 的错误处理。

像这样使用 generator 不易于扩展这一点是不言自明的。况且由于这种不是很自观的语法,你的迭代器代码需要和你使用的 generator 高度耦合。这意味着想要向你的 generator 中加入新的 await 表达式需要频繁修改代码。比较好的替代方法是使用即将到来的 async 函数

使用 async / await

使用 async 函数 时我们可以实现基于 Promise 的类似 generator 那样写同步代码的方式。另一个好处是你不需要修改 getRandomPonyFooArticle ,只要它返回的是一个 promise,它就可以使用 await 获取.

注意 await 只能用于标注了 async 关键字的函数内部 。它的工作方式类似 generator,在 promise 确定状态之前它的执行流程会挂起。如果 await 的不是 promise,会自动转化为一个 promise.

read();

async function read () {
  var html = await getRandomPonyFooArticle();
  var md = hget(html, {
    markdown: true,
    root: 'main',
    ignore: '.at-subscribe,.mm-comments,.de-sidebar'
  });
  var txt = marked(md, {
    renderer: new Term()
  });
  console.log(txt);
}

和 generator 一样,记住你应该把 await 部分放到 try / catch 里,用这种方式对 await 的那个 promise 进行错误捕获和处理。

另外,一个 Async 函数 总是返回一个 Promise . 未捕获异常会被这个 promise reject,否则 promise 会 resolve 这个 async 函数的返回值。因此我们可以调用一个 aysnc 函数并将其和 promise 的链式调用方法相结合,接下来的实例看看怎么组合使用这两者 (参见 Babel REPL) .

async function asyncFun () {
  var value = await Promise
    .resolve(1)
    .then(x => x * 3)
    .then(x => x + 5)
    .then(x => x / 2);
  return value;
}
asyncFun().then(x => console.log(`x: ${x}`));
// <- 'x: 4'

回到前面一个例子,这意味着我们可以在 async read 函数里 return txt , 这样用户可以接着使用 promise 或者另一个 async 函数。这样你的 read 函数只需要关注怎么从 Pony Foo 获取一篇随机文章并转换为终端可读的 markdown 形式。

async function read () {
  var html = await getRandomPonyFooArticle();
  var md = hget(html, {
    markdown: true,
    root: 'main',
    ignore: '.at-subscribe,.mm-comments,.de-sidebar'
  });
  var txt = marked(md, {
    renderer: new Term()
  });
  return txt;
}

然后,你可以进一步在另一个 Async 函数 里 await read() .

async function write () {
  var txt = await read();
  console.log(txt);
}

或者直接使用 promise 以进行更多后续处理。

read().then(txt => console.log(txt));`

重要抉择

在异步代码流程中,并行执行两个甚至多个任务的情形十分常见。 Async 函数 使编写异步代码变得简单,同时它们也可以用在 串行 的代码中,亦即,那些 同一时间只执行一个操作 的代码。内部包含多个 await 表达式的函数,在每个 await 表达式处都会挂起,直到 Promise 的状态确定并继续执行到下一个 await 表达式—— 这和我们观察到的 generator 和 yield 的行为略有不同

绕开这一点的办法是使用 Promise.all 创建一个单独的 promise ,然后 await 这个 promise. 当然,最大的问题是培养使用 Promise.all 的习惯而不是让所有事情都序列执行,后者会拖累你代码的性能表现。

接下来的例子展示如何 await 三个不同的 promise,同时让它们完全可以并发执行。 await 会挂起你的 async 函数并且 await Promise.all 表达式最终会 resolve 为一个 results 数组,我们可以通过解构拿到数组里单独的每一个结果。

async function concurrent () {
  var [r1, r2, r3] = await Promise.all([p1, p2, p3]);
}

在某段历史时期,上面的代码可以用 await* 来实现,你不需要将你的 promise 用 Promise.all 包起来, Babel 5 支持这个特性。但是因为