ECMAScript 6及之后的几个版本逐步加大了对异步编程机制的支持,提供了令人眼前一亮的新特性。ECMAScript 6新增了正式的Promise(期约)引用类型,支持优雅地定义和组织异步逻辑。接下来几个版本增加了使用async和await关键字定义异步函数的机制。

一、异步编程

异步编程是为了优化因计算量大而时间长的操作。如果在等待其他操作完成的同事,即使运行其他指令,系统也能保持稳定,那么这样做就是务实的。异步操作并不一定计算量大或要等很长时间。只要你不想为等待某个异步操作而阻塞线程执行,那么任何时候都可以使用。

1.1、同步与异步

同步行为对应内存中顺序执行的处理器指令。
相对地,异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。
异步编程经常是必要的,因为强制进行等待一个长时间的操作通常是不可行的(同步操作则必须要等)。
异步操作的例子可以是在定时回调中执行一次简单的数学计算:

  1. let x = 3;
  2. setTimeout(() => x = x + 4, 1000);

这段程序最终与同步代码执行的任务一样,都是把两个数加在一起,但这一次执行线程不知道x值何时会改变,因为这取决于回调何时从消息队列出列并执行。

1.2、以往的异步编程模式

在早期的JavaScript中,只支持定义回调函数来表明异步操作完成。串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称“回调地狱”)来解决。

  1. function double(value) {
  2. setTimeout(() => setTimeout(console.log, 0, value * 2), 1000);
  3. }
  4. double(3);

1000毫秒之后,JavaScript运行时会把回调函数推到自己的消息队列上去等待执行。推到队列之后,回调什么时候出列被执行对JavaScript代码就完全不可见了。
double()函数在setTimeout成功调度异步操作之后会立即退出。

1、异步返回值

假设setTimeout操作会返回一个有用的值。有什么好办法把这个值传给需要它的地方?广泛接受的一个策略是给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)。

  1. function double(value, callback) {
  2. setTimeout(() => callback(value * 2), 1000);
  3. }
  4. double(3, (x) => console.log(`I was given: ${x}`));

setTimeout调用告诉JavaScript运行时在1000毫秒之后把一个函数推到消息队列上。这个函数会由运行时负责异步调度执行。而位于函数闭包中的回调及其参数在异步执行时仍然是可用的。

2、失败处理

异步操作的失败处理在回调模型中也要考虑,因此自然就出现了成功回调和失败回调:

  1. function double(value, success, failure) {
  2. setTimeout(() => {
  3. try {
  4. ...
  5. } catch (e) {
  6. failure(e)
  7. }
  8. })
  9. }
  10. const successCallback = (x) => console.log(`sucess: ${x}`);
  11. const failureCallback = (e) => console.log(`Failure: ${e}`)
  12. double(3, successCallback, failureCallback);

这种模式已经不可取了,因为必须在初始化异步操作时定义回调。异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接收到它。

3、嵌套异步毁掉

如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。