十一章——期约与异步函数

一、异步编程

1. 同步与异步

  • 同步行为对应内存中顺序执行的处理器指令
  • 异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行

2. 以往的异步编程模式

  • 早期的JavaScript中,只支持定义回调函数来表明异步操作完成

    1. 业务简单 不需要使用到异步操作的返回值

      1
      2
      3
      4
      5
      6
      function double(value){
      setTimeout(() => {
      console.log(value * 2);
      }, 1000);
      }
      double(3)
    2. 需要用到异步操作的返回值如何处理? 传入一个回调函数

      1
      2
      3
      4
      5
      6
      7
      8
      function double(value,callback){
      setTimeout(()=>{
      callback(value * 2);
      },1000)
      }
      double(3, x => {
      console.log(`I was given: ${x}`);
      })
    3. 既然存在回调函数,那就会想到成功和失败分别如何处理 于是就需要指定两个回调函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      function double(value,success,failure){
      setTimeout(() => {
      try{
      if(typeof value !== 'number'){
      throw 'Must provide number as first argument'
      }
      success(2 * value);
      }catch(e){
      failure(e)
      }
      }, 1000);
      }
      const successCallback = x => {
      console.log(`Success: ${x}`);
      }
      const failureCallback = e => {
      console.log(`Failure: ${e}`);
      }
      double(3,successCallback,failureCallback);
      double('b',successCallback,failureCallback);
    4. 上述方法 存在几个问题 一是我们必须在异步操作之前就指定回调函数,二是返回值只在短时间内有效,并且如果下一个异步操作依赖于前一个异步操作的返回值就会产生回调地狱,也就是嵌套异步回调的情况

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      function double(value, success, failure) {
      setTimeout(() => {
      try {
      if (typeof value !== 'number') {
      throw 'Must provide number as first arguments'
      }
      success(2 * value)
      } catch (e) {
      failure(e)
      }
      }, 1000);
      }
      const successCallback = x => {
      double(x, y => {
      console.log(`Success ${y}`);
      })
      }
      const failureCallback = e => {
      console.log(`Failure ${e}`);
      }

      double(3,successCallback,failureCallback);

二、期约

1. Promise/A+规范

  • 早期的期约机制在jQuery和Dojo中是以Deferred API的形式出现的。

2. 期约基础

  • ECMAScript6新增的引用类型Promise,可以通过new操作符来实例化。

  • 创建新期约时需要传入执行器(executor)函数作为参数

    1
    2
    3
    4
    5
    // 空函数就是一个执行器函数 不指定会抛出异常
    let p = new Promise(()=>{});
    setTimeout(()=>{
    console.log(p); // Promise { <pending> }
    })
  • 期约状态机

    • 待定( pending )
    • 兑现( fulfilled,有时候也称为“解决”,resolved )
    • 拒绝( rejected )
    • 无论落定为哪种状态都是不可逆的,只要从待定转换为兑现或拒绝,期约的状态就不再改变
    • 期约的状态是私有的,不能直接通过JavaScript检测到。主要是为了避免根据读取到的期约状态,以同步方式处理期约对象
    • 期约的状态也不能被外部JavaScript修改,期约故意将异步行为封装起来,从而隔离外部的同步代码
  • 解决值、拒绝理由及期约用例

    • 每个期约只要状态切换为兑现,就会有一个私有的内部值(value)
    • 每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason)
    • 二者都是可选的,而且默认值为 undefined 在期约到达某个落定状态时执行的异步代码始终会收到这个值或理由。
  • 通过执行函数控制期约状态

    • 执行器函数有两项职责

      • 初始化期约的异步行为
      • 控制状态的最终转换——通过调用它的两个函数参数实现 resolve 和 reject
    • 执行器函数是同步执行的

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      new Promise(() => {
      setTimeout(() => {
      console.log('executor');
      }, 0);
      })
      setTimeout(() => {
      console.log('promise initialized');
      }, 0);
      // 结果:
      // executor
      // promise initialized
      1
      2
      3
      4
      5
      6
      7
      8
      9
      let p = new Promise((resolve,reject) => {
      setTimeout(() => {
      resolve();
      }, 1000);
      })
      setTimeout(() => {
      // 在打印期约实例的时候 还不会执行超时回调( 即 resolve() )
      console.log(p); // Promise { <pending> }
      }, 0);
    • 无论resolve()和reject()中的哪个被调用,状态转换都不可撤销了。于是继续修改状态会静默失败

      1
      2
      3
      4
      5
      6
      7
      8
      let p = new Promise((resolve,reject) => {
      resolve();
      reject();
      })
      setTimeout(() => {
      console.log(p);
      });
      // 这里打印的是 Promise { undefined } 但是可以从浏览器控制台里看出状态是fulfilled
  • Promise.resolve()

    • 期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态
    • 通过调用Promise.resolve()静态方法,可以实例化一个解决的期约
    1
    2
    3
    4
    5
    // 以下两个方法是等价的
    let p1 = new Promise((resolve,reject) => {
    resolve()
    })
    let p2 = Promise.resolve()
    • 这个解决的期约的值对应着传给Promise.resolve()的第一个参数。使用这个静态方法,实际上可以把任何值都转换为一个期约
    1
    2
    3
    4
    setTimeout(() => {
    // 多余的参数会忽略
    console.log(Promise.resolve(4,5,6,7)); // Promise { 4 }
    }, 0);
    • 对于静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此,Promise.resolve()可以说是一个幂等方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let p = Promise.resolve(7);

    setTimeout(()=>{
    console.log(p === Promise.resolve(p)); // true
    })

    setTimeout(() => {
    console.log(p === Promise.resolve(Promise.resolve(p))); // true
    });
    • 这个静态方法能够包装任何非期约值,包括错误对象,并将其转换为解决的期约。因此可能导致不符合预期的行为:
    1
    2
    3
    4
    let p = Promise.resolve(new Error('foo'));
    setTimeout(() => {
    console.log(p); // Promise <resolved>: Error: foo
    });
  • Promise.reject()

    • Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误( 这个错误不能通过 try / catch 捕获,而只能通过拒绝处理程序捕获 )
    • Promise.reject()没有照搬 Promise.resolve()的幂等逻辑。如果给它传一个期约对象,则这个期约会成为它返回的拒绝期约的理由
  • 同步 / 异步执行的二元性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    try{
    throw new Error('foo');
    }catch(e){
    console.log(e); // Error: foo
    }
    try{
    Promise.reject(new Error('bar'));
    }catch(e){
    console.log(e);
    }
    // Uncaught (in promise) Error: bar
    • 第一个try/catch抛出并捕获了错误,第二个try/catch抛出错误却没有捕获到
    • 从这里可以看出期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介

3. 期约的实例方法

  • 实现Thenable接口

  • Promise.prototype.then()

    • 为期约实例添加处理程序的主要方法
    • 接收两个参数 onResolved 处理程序和 onRejected 处理程序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    function onResolved(id) {
    setTimeout(() => {
    console.log(id, 'resolved');
    }, 0);
    }

    function onRejected(id) {
    setTimeout(() => {
    console.log(id, 'rejected');
    }, 0);
    }
    let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve()
    }, 3000);
    })
    let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
    reject()
    }, 3000);
    })
    p1.then(() => {
    onResolved('p1')
    }, () => {
    onRejected('p1')
    })
    p2.then(() => {
    onResolved('p2')
    }, () => {
    onRejected('p2')
    })

    //(3 秒后)
    // p1 resolved
    // p2 rejected
    • 两个参数是可选的,如果传了一个非函数类型的数据就会被忽略,如果你只想提供某一个参数,那么另一个参数最好设置为undefined 或 null,书中这里说undefined,但是后面的例子都是写的null,有点没搞明白
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    function onResolved(id){
    setTimeout(() => {
    console.log(id,'resolved');
    }, 0);
    }
    function onRejected(id){
    setTimeout(() => {
    console.log(id,'rejected');
    }, 0);
    }
    let p1 = new Promise((resolve,reject) => {
    setTimeout(() => {
    resolve();
    }, 3000);
    })
    let p2 = new Promise((resolve,reject) => {
    setTimeout(() => {
    reject();
    }, 3000);
    })

    // 非函数处理程序会被静默忽略,不推荐
    p1.then('hello')

    // 不穿onResolved处理程序的规范写法
    p2.then(null,()=>{
    onRejected('p2')
    })

    // p2 rejected (3秒后)
    • Promise.prototype.then() 方法返回一个新的期约实例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let p1 = new Promise(()=>{});
    let p2 = p1.then();
    setTimeout(() => {
    console.log(p1); // Promise { <pending> }
    }, 0);
    setTimeout(() => {
    console.log(p2); // Promise { <pending> }
    }, 0);
    setTimeout(() => {
    console.log(p1 === p2); // false
    }, 0);
    • 该处理程序的返回值会通过Promise.resolve()包装来生成新期约。如果没有提供这个处理程序,则 Promise.resolve()就会包装上一个期约解决之后的值。如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回值 undefined。
    • 总结一下就是分为以下几种情况
      • then里面什么都不传 或者传的是undefined、空的函数、新的Promise.resolve返回值都是Promise 处于resolved状态值为undefined
      • then里面有显示的返回值则Promise.resolve()会包装这个值
      • then里面如果抛出异常则会返回拒绝的期约

4. 期约扩展

三、异步函数

1. 异步函数

2. 停止和恢复执行

四、小结