jackieli123723 / jackieli123723.github.io

✅lilidong 个人博客
9 stars 0 forks source link

理解async-await #10

Open jackieli123723 opened 7 years ago

jackieli123723 commented 7 years ago

本周早些时候我们来看看ES2016的新功能。今天我们将了解async/ await。 该async/ await功能并没有获得晋级的ES2016,但这并不意味着它不会来为JavaScript。在撰写本文时,这是一个第三阶段的提案,并积极开展工作。该功能已经在Edge中,并且应该放在另一个浏览器中, 它将达到第四阶段 - 铺平了下一版语言(参见:TC39过程)。

我们已经听说过这个功能了一段时间了,但让我们深入了解一下,看看它是如何工作的。为了能够掌握这篇文章的内容,你需要对承诺和发电机的坚定的了解。这些资源可以帮助你。

使用承诺

我们假设我们有如下代码。在这里我正在包装一个HTTP请求Promise。这个承诺body在成功的情况下实现,err否则就被拒绝。它每次从这个博客中提取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);
    });
  });
}

以上所示承诺代码的典型用法如下。在那里,我们构建了一个承诺链,将HTML页面转换为Mark的一个子集,然后进入终端友好的输出,最后打印使用console.log。永远记住在.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));
}

当运行时,代码段生成输出,如下面的屏幕截图所示。

该代码“比使用回调更好”,当涉及到读取代码的顺序时。

使用发电机 我们已经探索发电机,作为过去以html合成“同步”方式提供的方式。即使该代码现在有些同步,但是涉及到相当多的包装,并且生成器可能不是最直接的方式来完成我们想要的结果,所以我们可能最终会坚持承诺。

let request = require('request');
let hget = require('hget');
let marked = require('marked');

function getRandomPonyFooArticle (gen) {
  var g = gen();
  g.next(); // Important! Otherwise stops execution on `var html = yield`.

  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 marked.Renderer()
  });

  console.log(txt);
});

请记住,您应该将yield呼叫包装在try/ catch块中,以保护我们在使用承诺时添加的错误处理。 不用说,使用这样的发电机不能很好地扩展。除了在组合中引入不直观的语法,您的迭代器代码将高度耦合到正在使用的生成器函数。这意味着您必须经常更改它,因为您将新的await表达式添加到生成器中。一个更好的选择是使用即将到来的异步功能。

使用async/await 当Async函数终于走上了路,我们将能够采取我们Promise的实现,并利用同步的生成器风格。这种方法的另一个好处是,getRandomPonyFooArticle只要返回一个承诺,就可以等待,您完全不必改变。

请注意,await只能在标有async关键字的功能中使用。它与发电机类似,在您的上下文中暂停执行,直到承诺落定。如果期待已久的表达不是一个承诺,它就是承诺。

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);
}

同样, - ,只是像发电机-记住,你应该换await在try/ catch让你可以捕捉并从内处理在等待承诺的错误async功能。 此外,Async函数总是返回一个Promise。在未捕获的异常的情况下,该承诺被拒绝,否则解决了该async函数的返回值。这使我们能够调用一个async功能,并将其与常规的基于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}`));

回到前面的例子,这意味着我们可以return txt从我们的async read功能,并允许消费者继续使用承诺或另一个异步功能。这样,您的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;
}
然后,您可以await read()在另一个Async功能中进一步。

async function write () {
  var txt = await read();
  console.log(txt);
}
或者你只能使用承诺进一步延续。

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

叉在路上

在异步代码流中,通常同​​时执行两个或多个任务。虽然Async功能使得编写异步代码更容易,但它们也适用于串行的代码。也就是说:一次执行一个操作的代码。一个具有多个await表达式的函数将在每个await表达式上一次暂停,直到它Promise被解决,然后再重新执行并移动到下一个await表达式- 与我们使用生成器和我们观察到的情况不同yield。

要解决这个问题,您可以使用它Promise.all来创建一个可以await启用的单一承诺。当然,最大的问题是习惯使用Promise.all而不是将所有内容都运行在一个系列中,否则会使代码的性能下降。

以下示例显示了如何await可以同时解决三个不同的承诺。鉴于await暂停您的async函数并且await Promise.all表达式最终解析成results数组,我们可以使用解构来从该数组中拉出单个结果。

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

在某些时候,有一个await*替代上面的代码,在那里你不必包裹你的承诺Promise.all。巴别5仍然支持它,但它是从规格(和从巴别6)掉下来- 因为原因。

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

你仍然可以做一些all = Promise.all.bind(Promise)比较简单的替代方法Promise.all。这样做的一个好处就是你可以做同样的事情Promise.race,而没有一个等同的await*。

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

错误处理 请注意,错误在一个async函数内“静默地”被吞噬- 就像正常的Promises一样。除非我们在表达式附近添加try/ catch阻止await,async否则无法捕获异常 - 无论是在函数体中被提出还是在暂停期间被抛出await- 将拒绝该async函数返回的承诺。

自然,这可以被看作是一种力量:你可以利用try/ catch惯例,你无法用回调来处理这些事情,而且有些能够与Promises进行交互。在这个意义上,Async功能类似于生成器,您也可以利用try/ catch感谢函数执行停止将异步流转换为同步代码。

此外,您可以从async函数外部捕获这些异常,只需.catch在其返回的承诺中添加一个子句即可。虽然这是将try/ catch错误处理风格与Promises中的.catch子句相结合的灵活方式,但它也可能导致混淆,并最终导致错误进入未处理。

read()
  .then(txt => console.log(txt))
  .catch(reason => console.error(reason));

我们需要小心和教育我们自己的不同方式,我们可以注意到异常,然后处理,记录或阻止它们。

使用async/ await今天 在今天的代码中使用Async函数的一种方法是通过Babel。这涉及到一系列模块,但是如果您愿意,您可以随时提出一个将所有这些模块包装在一个模块中的模块。我将其npm-run作为一种有用的方式来保存在本地安装的软件包中。


npm i -g npm-run
npm i -D \
  browserify \
  babelify \
  babel-preset-es2015 \
  babel-preset-stage-3 \
  babel-runtime \
  babel-plugin-transform-runtime

echo '{
  "presets": ["es2015", "stage-3"],
  "plugins": ["transform-runtime"]
}' > .babelrc

下面的命令将编译example.js通过browserify同时使用babelify,使支持异步功能。然后,您可以将脚本管道node或将其保存到磁盘。


npm-run browserify -t babelify example.js | node

进一步阅读 Async功能的规范草案令人惊讶的很短,如果您热衷于了解有关此即将到来的功能的更多信息,应该补充一个有趣的阅读。

我已经粘贴了一段代码,旨在帮助您了解async函数内部的工作原理。尽管我们不能填充工具新的关键字,它在理解发生的事情的幕后方面有帮助async/ await。

也就是说,了解Async函数内部利用发电机和承诺应该是有用的。 首先,接下来,下面的一点显示一个async function声明如何被放入一个常规的function返回spawn使用生成函数进给的结果- 我们将会考虑await作为句法等价物yield。

async function example (a, b, c) {
  example function body
}

function example (a, b, c) {
  return spawn(function* () {
    example function body
  }, this);
}

在spawn一个承诺中,围绕着将逐步通过用户代码(由用户代码组成的)生成器函数的代码,将值转发到“生成器”代码(async函数体)中。在这个意义上,我们可以看到,异步函数真的是语法糖在发生器和承诺之上,这使得您了解每个这些事情是如何工作的,以便更好地了解如何混合,匹配和将这些不同类型的异步代码流合并在一起。

function spawn (genF, self) {
  return new Promise(function (resolve, reject) {
    var gen = genF.call(self);
    step(() => gen.next(undefined));
    function step (nextF) {
      var next;
      try {
        next = nextF();
      } catch(e) {
        // finished with failure, reject the promise
        reject(e);
        return;
      }
      if (next.done) {
        // finished with success, resolve the promise
        resolve(next.value);
        return;
      }
      // not finished, chain off the yielded promise and `step` again
      Promise.resolve(next.value).then(
        v => step(() => gen.next(v)),
        e => step(() => gen.throw(e))
      );
    }
  });
}

突出显示的代码段应该有助于您了解async/ awaitalgorithm 如何迭代生成器序列(await表达式),将序列中的每个项目包裹在一个承诺中,然后用序列中的下一个步骤进行链接。当序列结束或其中一个承诺被拒绝时,底层生成函数返回的承诺得到解决。

jackieli123723 commented 6 years ago

try-catch 处理异常

const Router = require('koa-router')
const article = require('../services/article')
const user = require('../services/user')
let api = new Router()

api.get('/article/list', async (ctx) => {
  try {
    let data = await article.list()

    ctx.body = {
      success: true,
      message: 'success',
      data: data
    }
  } catch (err) {
    ctx.body = {
      success: false,
      message: err,
      data: []
    }
  }
})
module.exports = api