Promise是抽象异步处理对象以及对其进行各种操作的组件。 通俗点讲,Promise能解决由于回调嵌套带来的流程控制与可读性问题。

  1. 原生API函数的 Promise 化
  2. Promise.all 解决并行任务
  3. Promise.then 的链式调用
  4. 中断或取消 Promise 链
  5. 使用基于 Promise 的 async/await
  6. 小结

1. 原生API函数的 Promise 化

  • 大部分原生的API函数并不支持 Promise,还是基于回调来使用的,所以需要把一些方法改为返回一个 Promise 对象,这个过程被称为函数的 Promise 化。
  • 下面一个例子将对定时器 setTimeout Promise 化:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function timer (fn, time) {
    return function () {
    return new Promise( (resolve, reject) => {
    setTimeout(function () {
    fn();
    resolve();
    }, time);
    });
    }
    }

    Promise.resolve()
    .then(
    timer(function () {
    console.log('1')
    }, 1000)
    )
    .then(() => {
    console.log('2');
    });
    // 输出结果
    // 1
    // 2
  • Promise化本质上都属于一种Curry(柯里)化。Curry化是指,将需要传递多参数的函数生成一个新的函数,如上代码先通过执行 timer得到一个新的函数,该函数会返回一个Promise,这样就完成了Promise化。将一些基础的函数进行Promise化,可以
    大大减少不必要的代码。
  • 下面的代码,将会体现这种优势:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var promise_timer = timer(function () {
    console.log('1')
    }, 1000)
    function promise_timer2(){
    return new Promise( (resolve,reject) => {
    setTimeout(function () {
    console.log('1');
    resolve();
    }, 1000);
    });
    }

2. Promise.all 解决并行任务

  • 当某个函数需要在 N 个回调都完成时才执行,这个时候就可以使用 Promise.all 来改善你的代码。
  • 以下是一个图片并行加载的例子,当所有图片加载完成后,再将所有图片一起展示:
    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
    function loadImg(src){
    return new Promise( (resolve,reject) => {
    var img = document.createElement("img");
    img.src = src;
    img.onload = function(){
    resolve(img);
    }
    img.onerror = function(err){
    reject(err);
    }
    });
    }

    function showImgs(imgs){
    imgs.forEach(function(img){
    document.body.appendChild(img);
    });
    }

    Promise.all([
    loadImg('1.png'), //加载图片
    loadImg('2.png'),
    loadImg('3.png'),
    ...
    ]).then(showImgs); //显示图片
    • 需要注意的是,Promise.all 中传入的 Promise 数组,各自 resolve 之后得到的值,将合并成一个数组传入到 then 中的方法,且数组中 resolve 值的顺序,与 Promise 数组的顺序一致。
      promise

3. Promise.then 的链式调用

  • 在许多Promise示例中都可以看到类似如下的 链式调用 的代码:
    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    function getUserInfo(){
    console.log('getUserInfo start');
    return new Promise((resolve,reject)=>{
    setTimeout(()=>{
    console.log('getUserInfo end');
    var userInfo = {
    name: 'adam'
    };
    resolve(userinfo);
    },1000);
    });
    }

    function getGroupInfo(userinfo){
    console.log('getGroupInfo start');
    return new Promise((resolve,reject)=>{
    setTimeout(()=>{
    var groupInfo = {
    name: 'desc'
    }
    console.log('getGroupInfo end');
    resolve(groupInfo,userinfo);
    },1000);
    });
    }

    function getTaskInfo(groupInfo,userinfo){
    console.log('getTaskInfo start');
    return new Promise((resolve,reject)=>{
    setTimeout(()=>{
    var taskInfo = {
    name: 'rebuild'
    };
    console.log('getTaskInfo end');
    resolve();
    },1000);
    });
    }

    var p = Promise.resolve();
    p.then(getUserInfo)
    .then(getGroupInfo)
    .then(getTaskInfo);
    /* 输出结果
    getUserInfo start
    getUserInfo end
    getGroupInfo start
    getGroupInfo end
    getTaskInfo start
    getTaskInfo end
    */
    • 如上面代码所示,我们可以很清楚的理解到程序执行的顺序是:
      promise
  • 但是如果我们对代码进行一点小的改造,将 then 中的方法不再返回 Promise ,那么执行的代码将会变成这样:
    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
    36
    37
    38
    39
    40
    41
    42
    function getUserInfo(){
    console.log('getUserInfo start');
    new Promise((resolve,reject)=>{
    setTimeout(()=>{
    console.log('getUserInfo end');
    resolve();
    },1000);
    });
    }

    function getGroupInfo(){
    console.log('getGroupInfo start');
    new Promise((resolve,reject)=>{
    setTimeout(()=>{
    console.log('getGroupInfo end');
    resolve();
    },1000);
    });
    }

    function getTaskInfo(){
    console.log('getTaskInfo start');
    new Promise((resolve,reject)=>{
    setTimeout(()=>{
    console.log('getTaskInfo end');
    resolve();
    },1000);
    });
    }

    var p = Promise.resolve();
    p.then(getUserInfo)
    .then(getGroupInfo)
    .then(getTaskInfo);
    /* 输出结果
    getUserInfo start
    getGroupInfo start
    getTaskInfo start
    getUserInfo end
    getGroupInfo end
    getTaskInfo end
    */
    • 这是因为每次调用 then 都会返回一个新的 Promise ,如果 then 中的申明的方法没有返回一个 Promise ,那么会默认返回一个新的
      处于 fulfilledPromise ,之后添加的 then 中的方法都会立即执行,所以执行的顺序就变成这样了:
      promise
      promise
    • 当要在使用链式 Promise 时,请务必在 then 传入的方法中返回一个新的 Promise
    • 另外一个需要 注意 的是,resolve 传递给下个 then 方法的值只能有一个,上面 getTaskInfo 方法中是无法获取到 userInfo 的值,所以如果有多个值需要放在一个 数据集合( Array , Object , Map , Set )中传入下个方法。

4. 中断或取消 Promise 链

  • Promise 标准的 API 中并没有提供相应的方法来中断或者取消 Promise 链的执行,一些库中提供了类似 Promise.break 或者 Promise.fail 的方法来中断或取消 Promise 链。利用 Promise.catch 的特性来中断 promise 链。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /** 用于中断的信号 */
    class BreakSignal {}
    Promise
    .then(() => { // 1
    // 开始.
    })
    .then(() => { // 2
    if (wantToBreakHere) {
    // 抛出中断信号.
    throw new BreakSignal();
    }
    })
    .then(() => { // 3
    // 需要跳过的部分.
    })
    // 接住中断信号.
    .catch(BreakSignal, () => { // 4

    });
    • 只要在 Promise 执行过程中抛出异常,都会直接跳转到 catch 中。但是这样的做法有一个缺点,无法区分程序本身的异常,还是手动抛出的异常。所以需要手动设置一个标识标量,来区分是为了中断执行还是本身的程序异常。
    • 上面的代码中,执行到 2 时,由于抛出了终端信号,所以会直接跳过 3 直接执行 catch 部分。

5. 使用基于 Promise 的 async/await

  • async
    1
    2
    3
    async function f() {
    return 1
    }
    • 函数前面的 async 一词意味着一个简单的事情:这个函数总是返回一个 Promise,如果代码中有 return <非promise>语句,JavaScript会自动把返回的这个value值包装成promise的resolved值。
    • 例如,上面的代码返回resolved值为1的promise,我们可以测试一下:
      1
      2
      3
      4
      async function f() {
      return 1
      }
      f().then(alert) // 1
    • 我们也可以显式的返回一个promise,这个将会是同样的结果:
      1
      2
      3
      4
      async function f() {
      return Promise.resolve(1)
      }
      f().then(alert) // 1

      所以,async 确保了函数返回一个 Promise,即使其中包含非Promise。

  • await
    1
    2
    // 只能在 async 函数内部使用
    let value = await promise;
    • 关键词 await 可以让JavaScript进行等待,直到一个 Promise 执行并返回它的结果,JavaScript才会继续往下执行。
    • 以下是一个 Promise 在 1s 之后 resolve 的例子:
      1
      2
      3
      4
      5
      6
      7
      8
      async function f() {
      let p = new Promise((resolve, reject) => {
      setTimeout(() => resolve('done!'), 1000)
      })
      let result = await p // 直到promise返回一个resolve值(*)
      alert(result) // 'done!'
      }
      f()
    • 函数执行到(*)行会‘暂停’,当Promise处理完成后重新恢复运行, resolve的值成了最终的result,所以上面的代码会在1s后输出’done!’。

      我们强调一下:await 字面上使得JavaScript等待,直到 Promise 处理完成,然后将结果继续下去。这并不会花费任何的cpu资源,因为引擎能够同时做其他工作:执行其他脚本,处理事件等等。

  • 一个非常常见的栗子
    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
    function getSyncTime() {
    return new Promise((resolve, reject) => {
    try {
    let startTime = new Date().getTime();
    setTimeout(() => {
    let endTime = new Date().getTime();
    let data = endTime - startTime;
    resolve( data );
    }, 500);
    } catch ( err ) {
    reject( err );
    }
    });
    }

    async function getSyncData() {
    let time = await getSyncTime();
    let data = `endTime - startTime = ${time}`;
    return data;
    }

    async function getData() {
    let data = await getSyncData();
    // 打印
    console.log( data );
    }

    getData();

    /** 500ms 后输出
    * Promise {<pending>}
    * endTime - startTime = 501
    */

6. 小结

  • 放在一个函数前的 async 有两个作用:
    1. 使函数总是返回一个 Promise
    2. 允许在这其中使用 await
  • Promise 前面的 await 关键字能够使JavaScript等待,直到 Promise 处理结束。然后:
    1. 如果它是一个错误,异常就产生了,就像在那个地方调用了throw error 一样。
    2. 否则,它会返回一个结果,我们可以将它分配给一个值。
  • 有了 async/await,我们很少需要写 Promise.then/catch,但是我们仍然不应该忘记它们是基于 Promise 的,因为有些时候(例如在最外面的范围内)我们不得不使用这些方法。

参考文档 Promise