</lylijincheng>

Async/Await: JavaScript 中当之无愧的英雄

asyncawait

原文:Async/Await: The Hero JavaScript Deserved Eddie Zaneski,写于2015年10月2日

异步代码往往比较难写,一提到 JavaScript,我们会严重依赖回调函数来实现不太直观的异步任务。这种认知超载对新手编程产生了一个屏障,甚至会对使用了一段时间这门语言的人造成频繁的心痛。

这篇文章我们将尝试如何使用 ECMAScript 2016(ES7) 中的一个提案来改善 JavaScript 中异步编程的体验,使得代码更容易理解和书写。

现实的世界

我们先来看看现实的异步编程,下面的例子使用 request 库向罗恩斯旺森名言接口发起一个 HTTP 请求,然后再控制台打印出相应的内容。将下面的代码粘贴到一个名为 app.js 的文件中,然后运行 npm install request 安装依赖,如果你还没有安装 Node.js,可以在这里安装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var request = require('request');

function getQuote() {
  var quote;

  request('http://ron-swanson-quotes.herokuapp.com/quotes', function(error, response, body) {
    quote = body;
  });

  return quote;
}

function main() {
  var quote = getQuote();
  console.log(quote);
}

main();

如果你已经接触过 JavaScript 的异步编程,你可能已经明白为什么没有输出一条名言。如果是这样我会和你击掌。

high-five

运行 node app.js 你很快会发现输出了 undefined

为什么会这样

原因是 quote 变量的值是 undefined,因为在 request 函数执行完成时,给它赋值的回调函数还没有被调用。由于 request 函数操作是异步执行的,JavaScript 不会等他的执行结果,而是继续执行下面一条语句,返回了未赋值的变量。 如果想深入了解 JavaScript 异步执行是如何工作,可以看看 Philip Roberts 在 JSConf EU 上的一个很棒讨论

同步代码通常更容易理解和编写,因为所有的代码都是按编写的顺序执行的。return 语句在其他语言中被广泛使用并且相当直观,但是在 JavaScript 中我们不能同我们想要的那样使用它,因为这不太符合 JavaScript 的异步的本质。

为什么不与异步代码对抗呢?性能。网络请求和磁盘读取操作我们称之为 I/O(input/output) 操作,同步 I/O 执行会阻塞程序,停下来等待数据传输完成才返回。如果需要60秒去数据库查询,程序会停留在那里60秒,不做其他事情。然而在异步 I/O 操作中,程序会继续正常执行后面的代码,I/O 操作完成之后就会处理它的结果。这就是为什么回调函数会存在,但是在阅读程序源码的时候会感到难用和不好理解。

理想的世界

我们可以都有好的一面——可以很好地处理块级操作的异步代码,并且更容易阅读和书写?答案是肯定的。感谢 ES7 对异步函数(Async/Await)的 提案

当一个函数被声明为异步 async 时,它可以在 promise resolved 之前退出(yield)当前函数的执行,如果你不了解 promise,可以参考这些很棒的资料中的

app.js 中的代码替换为下面的代码,此外我们还需安装 Babel 转译工具来运行它。通过 npm install babel 安装 babel 模块,他会将 ES7 代码可以在当前环境可运行的代码,你可以在这里了解更多关于 Babel 的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var request = require('request');

function getQuote() {
  var quote;

  return new Promise(function(resolve, reject) {
    request('http://ron-swanson-quotes.herokuapp.com/quotes', function(error, response, body) {
      quote = body;

      resolve(quote);
    });
  });
}

async function main() {
  var quote = await getQuote();
  console.log(quote);
}

main();
console.log('Ron once said,');

可以看到我们将网络请求操包装为一个新函数 getQuote,返回了 promise 对象。

request 的回调函数中,我们调用 promise 的 resolve 函数来处理返回的结果。

执行下面的代码来运行新的例子

1
2
3
4
./node_modules/.bin/babel-node app.js

// Ron once said,
// {"quote":"Breakfast food can serve many purposes."}

哇,看起来非常酷,快要接近原来的期望了。尽管是异步的,它看起来已经非常像同步代码了。

如果你没注意到,Ron once said, 先被打印出来,尽管它是在 main 之后调用的。这表明但我们等待网络请求完成是并没有阻塞代码的执行。

实施改进

实际上我们可以用 try/catch 块增加错误处理来进一步改善它。如果在请求过程中出现错误,可以调用 promise 的 reject 函数,这将会在 main 里面捕获一个错误。和 return 语句一样,try/catch 块在过去很容易被理解,因为他们很难正确地同异步代码使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var request = require('request');

function getQuote() {
  return new Promise(function(resolve, reject) {
    request('http://ron-swanson-quotes.herokuapp.com/quotes', function(error, response, body) {
      if (error) return reject(error);
      resolve(body);
    });
  });
}

async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch(error) {
    console.error(error);
  }
}

main();
console.log('Ron once said,');

将请求 URL 修改为 http://foo 重新运行代码,你会发现捕获到一个异常。

好处

这些非常酷的优势能真正改边我们编写异步 JavaScript 的方式。可写出异步执行的代码,但是看起来是同步的,使其变得更容易使用常用的编程结构,比如 return 和 try/catch,会让语言变得更易懂。

最大的好处是可以通过返回 promise 对象,使用我们最喜欢的特性。我们看一下 Twilio Node.js library 的例子,如果你没有使用过 Twilio Node.js 库,可以在这里了解一下,你还需要一个 Twilio 账号,可在这里注册。

运行 npm install twilio,然后把下面的代码复制到一个名为 twilio.js 的文件中,将标记 // TODO 替换为你自己的凭证和号码。

1
./node_modules/.bin/babel-node twilio.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var twilio = require('twilio');

var client = twilio('YOUR_TWILIO_ACCOUNT_SID', 'YOUR_TWILIO_AUTH_TOKEN'); // TODO

async function sendTextMessage(to) {
  try {
    await client.sendMessage({
      to: to,
      from: 'YOUR_TWILIO_PHONE_NUMBER', // TODO
      body: 'Hello, Async/Await!'
    });
    console.log('Request sent');
  } catch(error) {
    console.error(error);
  }
}

sendTextMessage('YOUR_PHONE_NUMBER'); // TODO
console.log('I print out first to show I am async!');

和上面 getQuote 函数一样,我们将 sendTextMessage 标记为 async,这样可以使它 awaitclient.sendMessage 返回的 promise 对象成功的结果。

结束工作

我们已经看到如何利用 ES7 提案的特性来改善编写异步 JavaScript 的体验。

我非常高兴 Async/Await 提案能够继续向前推进,但是在等待期间,现在我们可以利用 Babel 结合任意返回 promise 对象来使用它。这则提案最近进入了候选阶段(第3步),需要被推广使用及反馈。如果你对它有不错的用法,记得在评论里面告诉我,或者 Twitter @eddiezane

Comments