查看原文
其他

用 Async 函数简化异步代码

前端大全 2020-01-12

(点击上方公众号,可快速关注)


英文:Joe Zimmerman&Nilson Jacques  译文:oschina

www.oschina.net/translate/simplifying-asynchronous-coding-es7-async-functions

如有好文章投稿,请点击 → 这里了解详情


Promise 在 JavaScript 上发布之初就在互联网上流行了起来 — 它们帮开发人员摆脱了回调地狱,解决了在很多地方困扰 JavaScript 开发者的异步问题。但 Promises 也远非完美。它们一直请求回调,在一些复杂的问题上仍会有些杂乱和一些难以置信的冗余。


随着 ES6 的到来(现在被称作 ES2015),除了引入 Promise 的规范,不需要请求那些数不尽的库之外,我们还有了生成器。生成器可在函数内部停止执行,这意味着可把它们封装在一个多用途的函数中,我们可在代码移动到下一行之前等待异步操作完成。突然你的异步代码可能就开始看起来同步了。


这只是第一步。异步函数因今年加入 ES2017,已进行标准化,本地支持也进一步优化。异步函数的理念是使用生成器进行异步编程,并给出他们自己的语义和语法。因此,你无须使用库来获取封装的实用函数,因为这些都会在后台处理。


运行文章中的 async/await 实例,你需要一个能兼容的浏览器。


运行兼容


在客户端,Chrome、Firefox 和 Opera 能很好地支持异步函数。



从 7.6 版本开始,Node.js 默认启用 async/await。


异步函数和生成器对比


这有个使用生成器进行异步编程的实例,用的是 Q 库:


var doAsyncOp = Q.async(function* () {

  var val = yield asynchronousOperation();

  console.log(val);

  return val;

});


Q.async 是个封装函数,处理场景后的事情。其中 * 表示作为一个生成器函数的功能,yield 表示停止函数,并用封装函数代替。Q.async 将会返回一个函数,你可对它赋值,就像赋值 doAsyncOp 一样,随后再调用。


ES7 中的新语法更简洁,操作示例如下:


async function doAsyncOp () {

  var val = await asynchronousOperation();     

  console.log(val);

  return val;

};


差异不大,我们删除了一个封装的函数和 * 符号,转而用 async 关键字代替。yield 关键字也被 await 取代。这两个例子事实上做的事是相同的:在 asynchronousOperation 完成之后,赋值给 val,然后进行输出并返回结果。


将 Promises 转换成异步函数


如果我们使用 Vanilla Promises 的话前面的示例将会是什么样?


function doAsyncOp () {

  return asynchronousOperation().then(function(val) {

    console.log(val);

    return val;

  });

};


这里有相同的代码行数,但这是因为 then 和给它传递的回调函数增加了很多的额外代码。另一个让人厌烦的是两个 return 关键字。这一直有些事困扰着我,因为它很难弄清楚使用 promises 的函数确切的返回是什么。


就像你看到的,这个函数返回一个 promises,将会赋值给 val,猜一下生成器和异步函数示例做了什么!无论你在这个函数返回了什么,你其实是暗地里返回一个 promise 解析到那个值。如果你根本就没有返回任何值,你暗地里返回的 promise 解析为 undefined。


链式操作


Promise 之所以能受到众人追捧,其中一个方面是因为它能以链式调用的方式把多个异步操作连接起来,避免了嵌入形式的回调。不过 async 函数在这个方面甚至比 Promise 做得还好。


下面演示了如何使用 Promise 来进行链式操作(我们只是简单的多次运行 asynchronousOperation 来进行演示)。


function doAsyncOp() {

  return asynchronousOperation()

    .then(function(val) {

      return asynchronousOperation(val);

    })

    .then(function(val) {

      return asynchronousOperation(val);

    })

    .then(function(val) {

      return asynchronousOperation(val);

    });

}


使用 async 函数,只需要像编写同步代码那样调用 asynchronousOperation:


async function doAsyncOp () {

  var val = await asynchronousOperation();

  val = await asynchronousOperation(val);

  val = await asynchronousOperation(val);

  return await asynchronousOperation(val);

};


甚至最后的 return 语句中都不需要使用 await,因为用或不用,它都返回了包含了可处理终值的 Promise。


并发操作


Promise 还有另一个伟大的特性,它们可以同时进行多个异步操作,等他们全部完成之后再继续进行其它事件。ES2015 规范中提供了 Promise.all(),就是用来干这个事情的。


这里有一个示例:


function doAsyncOp() {

  return Promise.all([

    asynchronousOperation(),

    asynchronousOperation()

  ]).then(function(vals) {

    vals.forEach(console.log);

    return vals;

  });

}


Promise.all() 也可以当作 async 函数使用:


async function doAsyncOp() {

  var vals = await Promise.all([

    asynchronousOperation(),

    asynchronousOperation()

  ]);

  vals.forEach(console.log.bind(console));

  return vals;

}


这里就算使用了 Promise.all,代码仍然很清楚。


处理拒绝


Promises 可以被接受(resovled)也可以被拒绝(rejected)。被拒绝的 Promise 可以通过一个函数来处理,这个处理函数要传递给 then,作为其第二个参数,或者传递给 catch 方法。现在我们没有使用 Promise API 中的方法,应该怎么处理拒绝?可以通过 try 和 catch 来处理。使用 async 函数的时候,拒绝被当作错误来传递,这样它们就可以通过 JavaScript 本身支持的错误处理代码来处理。


function doAsyncOp() {

  return asynchronousOperation()

    .then(function(val) {

      return asynchronousOperation(val);

    })

    .then(function(val) {

      return asynchronousOperation(val);

    })

    .catch(function(err) {

      console.error(err);

    });

}


这与我们链式处理的示例非常相似,只是把它的最后一环改成了调用 catch。如果用 async 函数来写,会像下面这样。


async function doAsyncOp () {

  try {

    var val = await asynchronousOperation();

    val = await asynchronousOperation(val);

    return await asynchronousOperation(val);

  } catch (err) {

    console.err(err);

  }

};


它不像其它往 async 函数的转换那样简洁,但是确实跟写同步代码一样。如果你在这里不捕捉错误,它会延着调用链一直向上抛出,直到在某处被捕捉处理。如果它一直未被捕捉,它最终会中止程序并抛出一个运行时错误。Promise 以同样的方式运作,只是拒绝不必当作错误来处理;它们可能只是一个说明错误情况的字符串。如果你不捕捉被创建为错误的拒绝,你会看到一个运行时错误,不过如果你只是使用一个字符串,会失败却不会有输出。


中断 Promise


拒绝原生的 Promise,只需要使用 Promise 构建函数中的 reject 就好,当然也可以直接抛出错误——在 Promise 的构造函数中,在 then 或 catch 的回调中抛出都可以。如果是在其它地方抛出错误,Promise 就管不了了。


这里有一些拒绝 Promise 的示例:


function doAsyncOp() {

  return new Promise(function(resolve, reject) {

    if (somethingIsBad) {

      reject("something is bad");

    }

    resolve("nothing is bad");

  });

}

 

/*-- or --*/

 

function doAsyncOp() {

  return new Promise(function(resolve, reject) {

    if (somethingIsBad) {

      reject(new Error("something is bad"));

    }

    resolve("nothing is bad");

  });

}

 

/*-- or --*/

 

function doAsyncOp() {

  return new Promise(function(resolve, reject) {

    if (somethingIsBad) {

      throw new Error("something is bad");

    }

    resolve("nothing is bad");

  });

}


一般来说,最好使用 new Error,因为它会包含错误相关的其它信息,比如抛出位置的行号,以及可能会有用的调用栈。


这里有一些抛出 Promise 不能捕捉的错误的示例:


function doAsyncOp() {

  // the next line will kill execution

  throw new Error("something is bad");

  return new Promise(function(resolve, reject) {

    if (somethingIsBad) {

      throw new Error("something is bad");

    }

    resolve("nothing is bad");

  });

}

 

// assume `doAsyncOp` does not have the killing error

function x() {

  var val = doAsyncOp().then(function() {

    // this one will work just fine

    throw new Error("I just think an error should be here");

  });

  // this one will kill execution

  throw new Error("The more errors, the merrier");

  return val;

}


在 async 函数的 Promise 中抛出错误就不会产生有关范围的问题——你可以在 async 函数中随时随地抛出错误,它总会被 Promise 抓住:


async function doAsyncOp() {

  // the next line is fine

  throw new Error("something is bad");

  if (somethingIsBad) {

    // this one is good too

    throw new Error("something is bad");

  }

  return "nothing is bad";

 

// assume `doAsyncOp` does not have the killing error

async function x() {

  var val = await doAsyncOp();

  // this one will work just fine

  throw new Error("I just think an error should be here");

  return val;

}


当然,我们永远不会运行到 doAsyncOp 中的第二个错误,也不会运行到 return 语句,因为在那之前抛出的错误已经中止了函数运行。


问题


如果你刚开始使用 async 函数,需要小心嵌套函数的问题。比如,如果你的 async 函数中有另一个函数(通常是回调),你可能认为可以在其中使用 await ,但实际不能。你只能直接在 async 函数中使用 await 。


比如,这段代码无法运行:


async function getAllFiles(fileNames) {

  return Promise.all(

    fileNames.map(function(fileName) {

      var file = await getFileAsync(fileName);

      return parse(file);

    })

  );

}


第 4 行的 await 无效,因为它是在一个普通函数中使用的。不过可以通过为回调函数添加 async 关键字来解决这个问题。


async function getAllFiles(fileNames) {

  return Promise.all(

    fileNames.map(async function(fileName) {

      var file = await getFileAsync(fileName);

      return parse(file);

    })

  );

}


你看到它的时候会觉得理所当然,即便如此,仍然需要小心这种情况。


也许你还想知道等价的使用 Promise 的代码:


function getAllFiles(fileNames) {

  return Promise.all(

    fileNames.map(function(fileName) {

      return getFileAsync(fileName).then(function(file) {

        return parse(file);

      });

    })

  );

}


接下来的问题是关于把 async 函数看作同步函数。需要记住的是,async 函数内部的的代码是同步运行的,但是它会立即返回一个 Promise,并继续运行外面的代码,比如:


var a = doAsyncOp(); // one of the working ones from earlier

console.log(a);

a.then(function() {

  console.log("`a` finished");

});

console.log("hello");

 

/* -- will output -- */

Promise Object

hello

`a` finished


你会看到 async 函数实际使用了内置的 Promise。这让我们思考 async 函数中的同步行为,其它人可以通过普通的 Promise API 调用我们的 async 函数,也可以使用它们自己的 async 函数来调用。


如今,更好的异步代码!


即使你本身不能使用异步代码,你也可以进行编写或使用工具将其编译为 ES5。 异步函数能让代码更易于阅读,更易于维护。 只要我们有 source maps,我们可以随时使用更干净的 ES2017 代码。


有许多可以将异步功能(和其他 ES2015+功能)编译成 ES5 代码的工具。 如果您使用的是 Babel,这只是安装 ES2017 preset 的例子。



觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存