你可能不知道的 Promise

本文首發於個人博客,轉載請註明出處:http://kohpoll.github.io/blog/2016/05/02/the-promise-you-may-not-know/javascript

開始以前,咱們先來一個「腦筋急轉彎」。假設 later 函數調用後返回一個 promise 對象,下面這 4 種寫法有什麼區別?html

// #1
later(1000)
  .then(function() {
    return later(2000);
  });

// #2
later(1000)
  .then(function() {
    later(2000);
  });

// #3
later(1000)
  .then(later(2000));

// #4
later(1000)
  .then(later);

你們能夠先實際去運行代碼看看輸出結果:#1#2#3#4,想一想爲何是這樣的輸出,而後回來繼續閱讀。java

Promise 是什麼

Promise 是對異步處理的一種抽象。在 JavaScript 中,咱們一般使用回調函數來進行異步處理:git

later(1000, function(err) {
  if (err) {
    // 失敗時的處理
  } else {
    // 成功時的處理
  }
});

Promise 將異步處理進行抽象,並造成規範化的接口:github

later(1000)
  .then(function() {
    // 成功時的處理
  })
  .catch(function(err) {
    // 失敗時的處理
  });

基於固定、統一的接口編程,咱們能夠將複雜的異步處理模式化。編程

建立 Promise 對象

  • new Promise(function fn(resolve, reject) {}) 會返回一個狀態爲 pending 的 promise 對象api

  • 在 fn 中指定處理邏輯數組

    • 處理成功時,調用 resolve(結果),promise 對象狀態變動爲 fulfilledpromise

    • 處理失敗時,調用 reject(Error 對象),promise 對象狀態變動爲 rejectedbash

const later = function(timeout) {
  return new Promise(function fn(resolve, reject) {
    timeout = parseInt(timeout, 10);
    if (!timeout) {
      return reject(new Error('Invalid Timeout'));
    }
    return setTimeout(function() {
      resolve('later_' + timeout);
    }, timeout);
  });
};

獲取到 promise 對象後,咱們能夠爲其添加處理方法:

  • 當對象被 resolve 時的處理方法(onFulfilled)

  • 當對象被 reject 時的處理方法(onRejected)

later(1000)
  .then(function onFulfilled(data) {})
  .catch(function onRejected(err) {});

later(1000)
  .then(
    function onFulfilled(data) {},
    function onRejected(err) {}
  );

在 onFulfilled 函數內能夠作什麼

later(1000).then(function() {
  // 在這裏能作些什麼特別的?
});
  • 返回一個 promise 對象

  • 返回一個同步值(什麼也不返回,那就是返回 undefined)

  • throw 一個 Error 對象

返回一個 promise 對象

返回一個 promise 對象就是在作異步操做的串行化。

later(1000)
  .then(function(data) {
    // data = later_1000
    return later(2000);
  })
  .then(function(data) {
    // data = later_2000
  });

上面的代碼會在 1 秒後得到第一個 promise 對象的結果,而後再過 2 秒後得到第二個 promise 對象的結果。

返回一個同步值

返回一個同步值能夠將同步代碼 「promise 化」。

later(1000)
  .then(function(data) {
    // data = later_1000
    if (data != 'later_1000') {
      return later(1000);
    }
    return data;
  })
  .then(function(data) {
    // data = later_1000
  });

上面的代碼保證最後得到的結果老是 later_1000(只是等 1 秒仍是 2 秒的區別)。更實用的例子是異步獲取某個數據(查詢 db),咱們能夠先從本地 cache 查詢,查到直接返回同步值,不然返回一個查詢 db 的 promise 對象,最終都會得到正確的數據。

函數什麼都不返回等於返回了 undefined,因此當心下面的寫法:

later(1000)
  .then(function(data) {
    // data = later_1000
    later(2000);
  })
  .then(function(data) {
    // data = undefined
  });

上面的代碼在 1 秒後取到第一個 promise 對象的結果,而後不會等待第二個 promise 對象的結果,立刻就執行到了第二個 .then()

(同步)拋出一個 Error 對象

.then() 裏面拋出一個 Error 對象可讓錯誤處理更加方便。

later(1000)
  .then(function(data) {
    if (data == 'later_1000') {
      throw new Error('later_1000 invalid');
    }
    if (data == 'later_2000') {
      return data;
    }
    return later(3000);
  })
  .then(function(data) {
  })
  .catch(function(err) {
    // 捕獲到錯誤 Error('later_1000 invalid');
  });

只要咱們調用 catch 添加了 onRejected 回調處理,在 then() 裏面 throw 出的任何同步錯誤都會在 catch() 裏面被捕捉到(好比:不當心訪問了未定義值啊、JSON.parse 錯誤啊等等),這讓問題定位很是方便。

能捕獲到的是「同步」錯誤,請當心下面的代碼:

later(1000)
  .then(function(data) {
    setTimeout(function() {
      throw new Error('the err can not catch');
    }, 1000);
  })
  .then(function(data) {
    // data = undefined
  })
  .catch(function(err) {
    // 捕獲不到錯誤
  });

另外須要注意 .then(null, onRejected) 並不徹底等價於 .catch(onRejected)

later(1000)
  .then(function(data) {
    throw new Error('this is err');
  })
  .catch(function(err) {
    // 捕獲到錯誤 Error('this is err');
  });

later(1000)
  .then(
    function(data) {
      throw new Error('this is err');
    },
    function(err) {
      // 捕獲不到錯誤 Error('this is err');
    }
  );

最佳實踐

咱們總結下這一部分的最佳實踐:

  • 老是在 .then() 裏面使用 return 來返回 promise 對象或者同步值

  • 老是在 .then() 裏面 throw 同步的 Error 對象

  • 老是使用 .catch() 來捕獲錯誤

能夠給 .then() 函數傳遞什麼

目前爲止,咱們看到給 .then() 傳遞的都是函數,可是其實它能夠接受非函數值:

later(1000)
  .then(later(2000))
  .then(function(data) {
    // data = later_1000
  });

.then() 傳遞非函數值時,實際上會被解析成 .then(null),從而致使上一個 promise 對象的結果被「穿透」。因而,上面的代碼等價於:

later(1000)
  .then(null)
  .then(function(data) {
    // data = later_1000
  });

爲了不沒必要要的麻煩,建議老是給 .then() 傳遞函數。

一些編碼技巧

快速建立 promise 對象

上面咱們主要經過 new Promise(fn) 的方式來建立 promise 對象,實際上有一個快捷方法 Promise.resolve(value) 能夠方便的建立 promise 對象。

Promise.resolve 的使用場景主要包括:

  • 用最少的代碼快速建立一個 promise 對象

  • 在 promise 化的 API 接口中將同步代碼 promise 化,更好的捕捉同步代碼產出的錯誤

下面兩種寫法是等價的,顯然使用 Promise.resolve 更加簡練:

new Promise(function(resolve, reject) {
  resolve('value');
}).then(function(data) {});

Promise.resolve('value').then(function(data) {});

在 promise 化的 API 中,將同步代碼 promise 化能夠統一的在 .catch() 中捕獲異常:

function apiReturnPromise() {
  return Promise.resolve().then(function() {
    someFuncMayThrowError();
    return 'xyz';
  }).then(function(data) {
    return doAsync(data);
  });
}
apiReturnPromise()
  .then(function(data) {})
  .catch(function(err) {
    // 能夠一致的捕捉到錯誤
  });

如何解決 promise 對象間的依賴

實際編碼中咱們可能常常遇到一個 promise 對象依賴另外一個 promise 對象的執行,而且咱們兩個 promise 對象的結果都須要的狀況:

later(1000)
  .then(function(dataA) {
    return later(2000);
  })
  .then(function(dataB) {
    // 咱們同時須要 dataA 和 dataB
  });

前面咱們說到在 .then() 裏能夠返回 promise 對象,這個 promise 對象其實是能夠調用本身的 .then() 的。下面的代碼能夠知足需求:

later(1000)
  .then(function(dataA) {
    return later(2000).then(function(dataB) {
      return dataA + ':' + dataB;
    });
  })
  .then(function(data) {
    // data = later_1000:later_2000
  });

異步處理中的並行和串行

在異步處理中,常常須要結合 for 循環來進行批量處理。由於處理都是異步的,這個過程就相應的被分爲了兩類:

  • 並行:一批異步操做同時執行

  • 串行:一批異步操做挨個執行(一個操做完成後下一個操做才繼續)

promise 對象是對異步操做的一個抽象表示,對 promise 對象進行不一樣的操做能夠達到異步處理的並行和串行。

並行

Promise.all 方法能夠接受一個數組(數組裏面的元素是 promise 對象)。當數組內全部 promise 對象變爲 fulfilled 狀態時,才調用 .then();數組內有任一個 promise 對象變爲 rejectec 狀態時,調用 .catch()

const promises = [1000, 2000, 3000].map(function(timeout) {
  return later(timeout);
});
Promise.all(promises)
  .then(function(data) {
    // data = ['later_1000', 'later_2000', 'later_3000']
  })
  .catch(function(err) {
  });

數組內 promise 對象所表示的異步操做是同時執行的,而且最後的結果和傳遞給 Promise.all 的數組的順序是一致的。因此,3 秒鐘後咱們取得的結果是一個值爲 ['later_1000', 'later_2000', 'later_3000'] 的數組。

串行

.then() 裏面返回一個 promise 對象就是一種串行。咱們須要構造一個相似下面這樣的 promise 對象:

promise1
  .then(function() { return promise2; })
  .then(function() { return promise3; })
  .then(...);

將一個數組轉換爲一個值,使用 reduce 能夠實現:

[1000, 2000, 3000]
  .reduce(function(promise, timeout) {
    return promise.then(function() {
      return later(timeout);
    });
  }, Promise.resolve())
  .then(function(data) {
    // data = later_3000
  })
  .catch(function(err) {
  });

上面代碼將如下面的方式來執行:

Promise.resolve()
  .then(function() { return later(1000); })
  .then(function() { return later(2000); })
  .then(function() { return later(3000) })
  .then(function(data) {
    // data = later_3000
  })
  .catch(function(err) {
  });

每一個 promise 對象表示的異步操做依次執行,最終結果將會在 6 秒後取得(只能取到最後一個 promise 對象的結果,若是都須要的話,須要單獨進行處理存儲)。

參考答案

問題 1

later(1000)
  .then(function() {
    return later(2000);
  })
  .then(done);

執行結果:

later(1000)  later(2000)            done(later_2000)
|-----1s-----|----------2s----------|

問題 2

later(1000)
  .then(function() {
    later(2000);
  })
  .then(done);

執行結果:

later(1000)  done(undefined)           
|-----1s-----|
             later(2000)
             |----------2s----------|

問題 3

later(1000)
  .then(later(2000))
  .then(done);

執行結果:

later(1000)  done(later_1000)
|-----1s-----|
later(2000)
|----------2s----------|

問題 4

later(1000)
  .then(later)
  .then(done);

執行結果:

later(1000)  later(later_1000)
|-----1s-----|
             throw new Error('Invalid Timeout')

參考資料

相關文章
相關標籤/搜索