Promise Race, 並不公平的 Race

Jayden.李發表於2019-03-04

前言

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 錯誤的情況。

  • Promise.race 一定要儘可能在所定義的 Promise 之後呼叫。

我們稍稍把 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)
複製程式碼
  • 在某些情況下,promise2 就算更快,也不一定返回其值。

我們再來對 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 中排在較慢的非同步操作之後,得到一個意外的返回結果的情況。

相關文章