理解Javascript的异步等待

目前async / await特性并没有被添加到ES2016标准中,但不代表这些特性将来不会被加入到Javascript中。在我写这篇文章时,它已经到达第三版草案,并且正迅速的发展中。这些特性已经被IE Edge支持了,而且它将会到达第四版,届时该特性将会登陆其他浏览器 -- 为加入该语言的下一版本而铺路(也可以看看:TC39进程)。

我们听说特性已经有一段时间了,现在让我们深入它,并了解它是如何工作的。为了能够了解这篇文章的内容,你需要对promise和生成器对象有深厚的理解。这些资源或许可以帮到你。

使用Promise

让我们假设我们有像下面这样的代码。在这里我将一个HTTP请求包装在一个Promise对象中。这个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);
    });
  });
}

上述的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));
}

当代码运行后,这段代码将产生像以下截图所示的输出。

上面那段代码就是“比用回调函数更好”的写法,它能让你感觉像在按顺序的阅读代码。

使用生成器(generator)

过去,通过探索,我们发现生成器可以用一种“同步”合成的方法来获得html。即使现在的代码有一些同步写法,其中还是涉及相当多的包装,而且生成器可能不是最直截了当的达到我们期望结果的方法,最终可能无论如何我们都会坚持改为使用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);
});

“请记住,在使用promise时,你应该将yield调用包装在try/catch块中来保留我们添加的错误处理器”

不说你也知道,像这样使用生成器并不容易扩展。除了涉及直观的语法的混入,你的迭代代码会高度耦合到生成器函数中,这将会降低扩展性。这表示你在添加新的await表达式到生成器中时需要经常修改它。一个更好的替代方案是使用即将到来的Async函数

使用async/await

当Async函数终于落地时,我们将可以采取基于Promise的实现方法并使用它的优点,即像写同步生成器一样写异步。这种做法的另一个好处是你完全不需要再去修改getRandomPonyFooArticle方法,在它返回一个承诺前,它会一直等待。

要注意的是,await只能在函数中用async关键字标记后才能使用 它的工作方式和生成器很相似,直到promise完成之前,会在你的上下文中暂停处理。如果等待表达式不是一个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);
}

“再次, -- 跟生成器一样 -- 记住,你最好把`await`包装到`try/catch`中,这样你就可以在异步函数中对返回后的promise进行错误捕获和处理。”

此外,一个Async函数总是会返回一个Promise对象。 这个promise在出现无法捕获的异常时会被拒绝,否则它会处理async函数的返回值。这就允许我们调用一个async函数并混入常规的基于promise的扩展。以下例子展示了两个方法的结合(看看Babel的交互式解释器)。

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‘

回到前一个例子中,那表示我们可以从异步读取函数中返回文本,并且允许调用者使用promise或另一个Async函数进行扩展。 那样,你的读取函数将只需关注从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表达式的函数在promise对象执行完之前,在恢复执行和移动到下一个await表达式之前,会在每个await表达式处暂停一次, -- 就跟我们在生成器和yield关键字处观察到的情况一样。

你可以使用Promise.all来解决创建单个promise对象并进行等待的功能。 当然,最大的问题是从习惯于让所有东西都串行运行改成使用Promise.all, 否则这将给你的代码带来性能瓶颈。

下面的例子展示了你如何同时完成对三个不同的promise对象进行等待操作。特定的await操作符会暂停你的Async函数,和等待 Promise.all表达式一起,最终会被解析到一个结果数组中,我们可以使用析构函数逐个拉取该数组中的单个结果。

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

在某些情况下, 可以用 await *来改动上述代码片段,让你不必用Promise.all来包装你的promise对象。Babel 5依然支持这种特性,但它已经从规格说明中移除(也已经从Babel 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])`;
}

错误处理

要注意的是,在异步函数中,错误会被“默默的”吞噬 -- 就像在普通的Promise对象中一样。 除非我们围绕await表达式添加try/catch块 -- 而不管在暂停时,它们会在你的异步函数体中发生还是在它暂停时发生 -- promise对象会被拒绝并通过Async函数返回错误。

自然,这可以看作是一个能力: 你可以利用try/catch代码块,有些东西你无法用回调函数实现-- 但可以用Promise对象实现。 在这种情况下,Async函数就类似生成器,得益于函数执行暂停特性,你可以利用try/catch将异步流代码写成同步代码的样子。

此外, 你可以在Async函数外捕获这些异常, 只需要简单的对它们返回的promise对象添加.catch()方法调用。在promise对象中尝试用.catch方法来将try/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

在使用babelifyAsync函数提供支持时,以下命令会将example.js通过browserify进行编译。然后你就可以用管道将脚本传输给node执行,或将脚本保存到硬盘中。

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

深入阅读

Async函数规格草案出奇的短,并且应该能成为一个有趣的读物, 如果你热衷于学习更多这些即将到来的功能。

我已经粘贴了一段代码在下面, 它是为了帮助你理解async函数的内部是如何工作的。即使我们不可以填充新的关键字,它也可以帮助你理解在async/await的帷幕后面发生了什么事情。

“换句话说,它应该对学习异步函数内部原理非常有帮助,无论是对生成器还是promise。”

首先,下面的一小段代码展示了一个async函数如何通过常规的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中,promise会被代码包装起来并传入生成器函数中,通过用户代码串行的执行,并将值传递到你的“生成器”代码中(async函数的函数体中)。 在这个意义上,我们可以注意Async函数真的是生成器和primose对象之上的语法糖,这对于让你理解其中每一个环节是如何工作来说非常重要,这是为了让你对于混合、匹配、合并不同的异步代码流的写法有一个更好的理解。

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) {
        // 执行失败,并拒绝promise对象
        reject(e);
        return;
      }
      if (next.done) {
        // 执行成功,处理promise对象
        resolve(next.value);
        return;
      }
      // 未完成,以yield标记的promise对象呗中断,并在此执行step方法
      Promise.resolve(next.value).then(
        v => step(() => gen.next(v)),
        e => step(() => gen.throw(e))
      );
    }
  });
}

“高亮部分的代码可以帮助你理解`async/await`如何对生成器序列进行迭代处理(通过`await`表达式),将每个对象按包装序列中并放入一个promise对象,通过一步步按顺序的链接起来。当**序列完成或其中一个promise对象被拒绝**时,promise对象的返回动作将由底层生成器函数完成”

特别鸣谢 @ljharb@jaydson@calvinf@ericclemmons@sherman3ero@matthewmolnar3以及@rauschma为这篇文章的草稿进行审阅。



第一次用博客园的markdown编辑器写博客,还不太顺手,对markdown也不够熟悉(其实我也感觉博客园的markdown解析怪怪的)。这篇文章的英语感觉比较难看懂,翻译不准确甚至错误的地方应该很多,希望大家不吝指正。

2016.09.05
谢谢观看。

时间: 2024-10-20 02:00:47

理解Javascript的异步等待的相关文章

理解Javascript的异步

前言 本文2925字,阅读大约需要10分钟. 总括: 本文梳理了异步代码和同步代码执行的区别,Javascript的事件循环,任务队列微任务队列等概念. 原文地址:Understanding Asynchronous JavaScript 公众号:「前端进阶学习」,回复「666」,获取一揽子前端技术书籍 未曾失败的人恐怕也未曾成功过. Javascript是单线程的编程语言,单线程就是说同一时间只能干一件事.放到编程语言上来说,就是说Javascript引擎(执行Javascript代码的虚拟机

JavaScript异步编程(一) 深入理解JavaScript事件

JavaScript异步编程 深入理解JavaScript事件 ?事件的调度 JavaScript事件处理器在线程空闲之前不会运行 线程的阻塞 var start = new Date(); // setTimeout和setInterval的计时精度比期望值差 setTimeout(function(){ var end = new Date(); console.log('Time elapsed', end - start, 'ms'); }, 500); while(new Date -

全面理解Javascript中Promise

全面理解Javascript中Promise 最近在学习Promise的时候,在网上收集了一些资料,发现很多的知识点不够系统,所以小编特意为大家整理了一些自认为 比较好的文章,供大家更好地学习js中非常有趣的Promise Promise概念 2015 年 6 月,ECMAScript 6 的正式版 终于发布了. ECMAScript 是 JavaScript 语言的国际标准,javascript 是 ECMAScript 的实现.ES6 的目标,是使得 JavaScript 语言可以用来编写大

重新理解javascript回调函数

把函数作为参数传入到另一个函数中.这个函数就是所谓的回调函数 经常遇到这样一种情况,某个项目的A层和B层是由不同的人员协同完成.A层负责功能funA,B层负责funcB.当B层要用到某个模块的数据,于是他对A层人员说,我需要你们提供满足某种需求的数据,你给我提供一个接口. A层的人员说:我给你提供数据,怎么展示和处理则是B的事情. 当然B层不可能为你每个需求都提供一个数据接口,B给A提供一个通过的接口.B得到数据,然后B写函数去展示. 即,你需要和其他人合作,别人提供数据,而你不需要关注别人获取

理解 Javascript 的单线程,着实不易

理解 Javascript 的单线程,着实不易. 比如,对于 C 系编程来说,写个 while(true) 不是什么令人紧张的事情.在 nodejs 中似乎也不会出什么意外,因为还有消息机制可以操控--这只是你对 nodejs 的幻觉,或者说,这只是我对 nodejs 的幻觉.若子进程写成这样的话: process.on('message', function() { while(true); }); process.on('SIGINT', function() { console.log('

理解javascript 回调函数

原文:理解javascript 回调函数 ##回调函数定义 百度百科:回调函数 回调函数就是一个通过函数指针调用的函数.如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数.回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应. 在JavaScript中,回调函数具体的定义为:函数A作为参数(函数引用)传递到另一个函数B中,并且这个函数B执行函数A.我们就说函数A叫做回调函数.如

JavaScript可否多线程? 深入理解JavaScript定时机制

http://www.phpv.net/html/1700.html JavaScript的setTimeout与setInterval是两个很容易欺骗别人感情的方法,因为我们开始常常以为调用了就会按既定的方式执行, 我想不少人都深有同感, 例如 setTimeout( function(){ alert(’你好!’); } , 0); setInterval( callbackFunction , 100); 认为setTimeout中的问候方法会立即被执行,因为这并不是凭空而说,而是Java

【前端_js】理解 JavaScript 的 async/await

async 和 await 在干什么 任意一个名称都是有意义的,先从字面意思来理解.async 是“异步”的简写,而 await 可以认为是 async wait 的简写.所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成. 理解 JavaScript 的 async/await 原文地址:https://www.cnblogs.com/leiblog/p/11057896.html

深入理解javascript作用域系列第一篇——内部原理

× 目录 [1]编译 [2]执行 [3]查询[4]嵌套[5]异常[6]原理 前面的话 javascript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域.作用域貌似简单,实则复杂,由于作用域与this机制非常容易混淆,使得理解作用域的原理更为重要.本文是深入理解javascript作用域系列的第一篇——内部原理 内部原理分成编译.执行.查询.嵌套和异常五个部分进行介绍,最后以一个实例过程对原理进行完整说明 编译 以var a = 2;为例,说明javasc