天天看點

Promise Race, 并不公平的 Race

前言

Promise Race 方法是我們在使用 Promise 的時候比較容易使用的一個方法。按照 MDN 對 Promise Race 的定義如下,

The Promise.race(iterable) method returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects, with the value or reason from that promise.

按照其字面意思了解,Promise.race 方法會傳回一個 Promise, 這個 Promise 是一個已經被 resolved 的。 其被 resolved 值為最快被 resolved 的 Promise 的值或者被 rejected 的值。

換句話說, 就是給予 Promise.race 一個 Promise 數組作為輸入, 其中最快被 resolved 的值作為傳回值 Promise 的 resolve 或者 rejected 值。

在 MDN 中所貼出來的代碼例子如下:

var promise1 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 500, 'one');
});

var promise2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then(function(value) {
  console.log(value);
  // Both resolve, but promise2 is faster
});
// expected output: "two"
           

容易造成的誤解

在上面的代碼中,有一句注釋,“Both resolve, but promise2 is faster”, 是以期望的結果是 "two"。這裡會給我們造成一種錯覺,就是哪個promise快,就一定傳回其 resolve 值。其實在這裡是有一些前提條件的。

  1. Promise.race 一定要盡可能在所定義的 Promise 們 之後調用。
  2. 在某些情況下,promise2 就算更快,也不一定傳回其值。

下面詳細講一下上面所說的兩種容易造成 Promise.race 錯誤的情況。

我們稍稍把 MDN 的代碼做一些改動,讓 Promise.race 不立即執行,而是在下一個執行周期去執行。

var promise1 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 500, 'one');
});

var promise2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 100, 'two');
});

// 在這裡,我使用了一個定時器回調 Promise.race 方法。
// 這個定時器的時間正好為兩個 promise 所要等待時間的最長時間,即500ms。
// 這時, console.log(value)的值隻和第一個 promise 相關聯,
// 就算 promise2 比 promise1 快,傳回的結果還是 “one”
setTimeout(()=> {
  Promise.race([promise1, promise2]).then(function(value) {
    console.log(value);
    });
}, 500)
           

我們再來對 MDN 的代碼做一些調整,如下:

var promise1 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 1, 'one');
});

var promise2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 0, 'two');
});



Promise.race([promise1, promise2]).then(function(value) {
  console.log(value);
  // Both resolve, but promise2 is faster
});           

上面的代碼比較極端,但是也能反應一些事情。 promise2 依然更快,但是傳回的結果确是 “one”。(這個地方很有可能和setTimeout的機制有關,我把 promise1 設定為大于等于2時,傳回結果為“two”。希望有知道的大神補充說明一下。我在以後會繼續研究setTimeout的相關運作機制。)

原罪

為什麼會造成上面的錯誤結果呢?我們可以先來看看 Promise.race 的實作源代碼。

cdn.jsdelivr.net/npm/es6-pro…
function race(entries) {
  /*jshint validthis:true */
  var Constructor = this; // this 是調用 race 的 Promise 構造器函數。

  if (!isArray(entries)) {
    return new Constructor(function (_, reject) {
      return reject(new TypeError('You must pass an array to race.'));
    });
  } else {
    return new Constructor(function (resolve, reject) {
      var length = entries.length;
      for (var i = 0; i < length; i++) {
        Constructor.resolve(entries[i]).then(resolve, reject);
      }
    });
  }
}
           

是以 race 的實作原理就是循環周遊 [promise1, promise2, ...], 并按照順序去 resolve 各個 promise. 注意:這裡是按照順序周遊,是以 race 不是嚴格意義的公平 race, 也就是說總有人先搶跑。在這裡 promise1 首先執行其 executor, 然後在調用 race 的時候,又首先被 Promise.race 周遊。 是以,首先定義的 promise 和放在數組前面的 promise 總是最先具有機會被 resolve。

是以,在這裡,如果我們沒有考慮到 race 的這種順序周遊 promise 執行個體的特性,就有可能得到意外的結果,正如在上面我所列出的反例所得到的結果。

第一個列子中,promise1 理論上在500毫秒後 resolve 結果,promise2 理論上在100毫秒後 resolve 結果。我給 Promise.race 設定了一個500毫秒的timer. 這個500毫秒的 timer 給了 promise1 充分的時間去 resolve 結果,是以就算 promise2 resolve 更快,但是得到的結果還是 promise1 的結果。

而在第二個例子中,我的了解是,當調用 Promise.race 時,根據上面 race 的源代碼我們可以知道,race 會通過給 then 傳遞 resolve 的方式,來把最先完成的 Promise 值給 resolve。 而 then 這種方法是一個異步方法,意思即為調用 then 以後,不管是 resolve,還是 reject 都是在下一個周期去執行,是以這就給了一些短期能夠結束的 Promise 機會。這樣,如果 Promise 中的 setTimeout 的時間足夠短的話,那麼在第一次調用 then 時, 前面的 Promise 首先 resolve 掉的話,就算數組後面的 Promise 的 setTimeout 時間更短,那麼也隻會 resolve 最先 resolved 的值。

結論

為了在使用 Promise 不造成非預期結果,我們需要盡量在定義完 Promise 們後,立即調用 Promise.race。其實,這一條建議也不是完全能保證 Promise.race 能夠公平地傳回最快 resolve 的值,比如:

let promises = [];

for (let i = 30000; i > -1; i-- ) {
  promises.push(new Promise(resolve => {    
    setTimeout(resolve, i, i);
  }))
}

Promise.race(promises).then(function(value) {
  console.log(value);
  // Both resolve, but promise2 is faster
});
           

雖然 Promise.race 在定義完所有 promise 後立即調用,但是由于 Promise 巨大的數量,超過一定臨界值的話,這時,resolve 出來的值就和周遊順序以及執行速度有關了。

總之,Promise.race 是順序周遊,而且通過 then 方法,又把回調函數放入了 event queue 裡, 這樣, 回調函數又要經曆一遍順序調用,如果 event queue 裡的 then 的回調方法都還沒有執行完畢的話,那麼 Promise.race 則會傳回最快的 resolve 值,但是一旦某些執行較快的異步操作在所有 then 回調函數周遊完畢之前就得到了傳回結果的話,就有可能造成,異步傳回速度雖然快,但是由于在 event queue 中排在較慢的異步操作之後,得到一個意外的傳回結果的情況。

原文釋出時間為:2018年06月24日

原文作者:Jayden.李

本文來源: 

掘金 https://juejin.im/entry/5b3a29f95188256228041f46

如需轉載請聯系原作者