Promise Race, 并不公平的 Race

5,375 阅读5分钟

前言

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 中排在较慢的异步操作之后,得到一个意外的返回结果的情况。