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 错误的情况。

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



原文发布时间为:2018年06月24日
原文作者:Jayden.李

本文来源: 掘金 如需转载请联系原作者




相关文章
|
6月前
|
前端开发
21 # 实现 promise 的 race 方法
21 # 实现 promise 的 race 方法
19 0
|
前端开发
前端学习案例12-promise&race&all之2
前端学习案例12-promise&race&all之2
47 0
前端学习案例12-promise&race&all之2
|
前端开发
前端学习案例11-promise&race&all之1
前端学习案例11-promise&race&all之1
40 0
前端学习案例11-promise&race&all之1
|
前端开发
Promise源码解密-catch/resolve/reject/race
Promise源码解密-catch/resolve/reject/race
|
前端开发 JavaScript
javascript 中promise对象中的all,allSettled,any,race方法
javascript 中promise对象中的all,allSettled,any,race方法
|
1月前
|
前端开发 JavaScript
如何处理 JavaScript 中的异步操作和 Promise?
如何处理 JavaScript 中的异步操作和 Promise?
15 1
|
1月前
|
前端开发 JavaScript
在JavaScript中,什么是promise、怎么使用promise、怎么手写promise
在JavaScript中,什么是promise、怎么使用promise、怎么手写promise
23 4
|
1月前
|
前端开发 JavaScript 开发者
JavaScript 中的异步编程:Promise 和 Async/Await
在现代的 JavaScript 开发中,异步编程是至关重要的。本文将介绍 JavaScript 中的异步编程概念,重点讨论 Promise 和 Async/Await 这两种常见的处理异步操作的方法。通过本文的阐述,读者将能够更好地理解和应用这些技术,提高自己在 JavaScript 开发中处理异步任务的能力。
|
2月前
|
前端开发 JavaScript API
JavaScript学习笔记(一)promise与async
JavaScript学习笔记(一)promise与async
|
2月前
|
前端开发 JavaScript UED
JavaScript中的异步编程和Promise
【2月更文挑战第3天】在Web开发中,JavaScript是一门非常重要的编程语言,而异步编程是JavaScript中的一个关键概念。本文将介绍JavaScript中的异步编程特点,以及如何使用Promise来更加优雅地处理异步操作,帮助开发者更好地理解和应用这一技术。
17 3