在前端 hybrid 应用内做“首屏接口数据预取”优化时,可能存在首屏接口数据竞速的场景,即希望能从 2 个数据源同时获取数据,并取最快的那份数据进行渲染。
但在此“数据竞速”场景下,由于我们需要对两个数据源返回的数据进行合法性校验,因此 promise 自带的方法不能完全适用,因为他无法满足我们“带数据合法性校验的竞速请求”的场景。这里我们探讨并给出一种实现。
场景解释
所谓“带数据合法性校验的竞速请求”是指的:我要同时发出 2 个请求,且希望尽快拿到最早返回的“符合数据合法性校验要求”的请求结果。
举例来说,假如我打开页面后,希望拿到一个数据,这份数据同时放到了“cdn 服务器地址”和“服务端后台地址”,于是我希望打开页面后立刻向 2 个地址发出请求并拿到最早的那个符合要求的结果进行展示。
这里所谓的“最早的符合要求”,是指的要满足 2 点要求:
- 它是尽可能早返回的那个数据,因为页面需要尽早拿到合法数据进行渲染。
- 返回的这个数据需要满足“数据合法性校验函数”。因为最先拿到的远程的数据不一定准确,所以我们要求必须返回那个“既合法,又早”的数据。
Promise 自带的方法
Promise 自带的有 Promise.race, Promise.all, Promise.any 等,但是却无法满足该场景。
- race 函数,确实可以实现竞速。但是它在任何一个请求最早 settle 的时候就会立刻交付结果。这个交付的结果可能是“失败的”结果。而我们期望如果最早的那个请求失败了,那应该向外交付后面那个结果。毕竟在页面渲染场景下:即使渲染慢一点,也不要 fail。
- all 函数。它需要等到所有 promise 全部敲定后,才会交付结果。这无法满足我们“尽快”的要求。
- any 函数。这是较新版本浏览器才支持的功能,它可以在任意一个 promise 能达到 resolve 的情况下则交付结果,否则会等其他 promise,直到有一个 resolve 或者全部都无法完成才交付结果。这个看起来能满足我们“尽快且成功的”要求,但是无法满足“尽快+合法”的要求。在我们场景下,还需要 any 函数中帮我们做一下数据合法性校验,若数据不合法,也应当继续等待其他请求。
2025-03-26 更新
其实后来我发现 Promise.any 是可以解决这个问题的,我们只需要在 fetchA 和 fetchB 两个 promise 上做点手脚之后,再传递给 Promise.any 就可以了。解决方案如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | function getRaceData() {const correctWrap = (res) => {
 if (res.includes("client")) return Promise.reject(res);
 else return res;
 };
 
 const clientRequest = fetchApi("http://www.baidu.com/client", 1000).then(
 correctWrap
 );
 const httpRequest = fetchApi("https://www.baidu.com/http", 3000).then(
 correctWrap
 );
 
 const res = await Promise.any([clientRequest, httpRequest]);
 return res;
 }
 
 | 
至此其实 Promise.any 就解决问题。如果您还想看我当初手工实现的思路,则可以继续往下阅读。
自己实现第一版
思路
俩请求同时发出,但是响应的时候,要先校验是否合法,合法则认为成功,成功就立刻 resolve 尽量让外面赶紧使用这份成功数据; 若失败则要看看是否还要等,没必要等的情况下才 reject—也就是只有最后一个 promise 才可能 reject。
总结来看:
- 成功后,不管三七二十一,直接 resolve 即可。这里利用 promise 状态不变的特性,自然可以避免多次 resolve 发生。
- 失败后,需要看“是否需要继续等待”再决定 reject。所谓“是否有必要继续等”,是需要看看是否还有可以继续等待的其他 promise。实现上,可以靠计数来实现。
具体代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 
 | function raceForTheBest(requestOne, requestTwo, checkFunc) {return new Promise((resolve, reject) => {
 let finishCount = 0;
 requestOne()
 .then((res) => {
 _onSuccess(res);
 })
 .catch((err) => {
 _onError(err);
 });
 
 requestTwo()
 .then((res) => {
 _onSuccess(res);
 })
 .catch((err) => {
 _onError(err);
 });
 
 function _onSuccess(res) {
 finishCount++;
 if (checkFunc(res)) {
 return resolve(res);
 } else {
 if (finishCount === 2) {
 reject(new Error("竞速请求数据全部都不符合要求"));
 }
 }
 }
 function _onError(err) {
 finishCount++;
 if (finishCount === 2) {
 reject(err);
 }
 }
 });
 }
 
 | 
上述代码中,requestOne 和 requestTwo 需要重复编写处理代码,所以我门把他抽到了 _onSuccess 和 _onError 函数里面来复用。
优化代码
上述代码 onSuccess 和 onError 其实也存在多次重复调用。实际上 requestOne 和 requestTwo 可以靠循环来生成 2 个独立的调用上下文,但我们代码就可以只写一次。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 
 | 
 function raceForTheBestRequest(one, two, checkFunc) {
 return new Promise((resolve, reject) => {
 const promiseArr = [one, two];
 const settledPromise = [];
 promiseArr.forEach((p) => {
 p()
 .then((res) => {
 settledPromise.push(p);
 if (checkFunc(res)) {
 resolve(res);
 } else {
 console.log("数据校验不通过,扔出");
 
 _onThrowError(new Error("数据格式不符合要求"));
 }
 })
 .catch((err) => {
 console.log("等到一个错误", err);
 settledPromise.push(p);
 
 _onThrowError(err);
 });
 });
 function _onThrowError(err) {
 if (settledPromise.length === promiseArr.length) {
 reject(err);
 }
 }
 });
 }
 
 | 
测试用例
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 
 | 
 const checkFunc = function (res) {
 return res.code === 0;
 };
 
 const p1 = function () {
 return new Promise((resolve) => {
 setTimeout(() => {
 resolve({
 code: 0,
 from: 1,
 });
 }, 400);
 });
 };
 
 const p2 = function () {
 return new Promise((resolve, reject) => {
 setTimeout(() => {
 resolve({
 code: 0,
 from: 2,
 });
 }, 1000);
 });
 };
 
 | 
不断修改 code 的值,或者修改 setTimeout 时间,多次尝试执行上述代码来进行测试。确保符合预期即可。