一种带数据合法性校验的竞速请求逻辑封装

在前端 hybrid 应用内做“首屏接口数据预取”优化时,可能存在首屏接口数据竞速的场景,即希望能从 2 个数据源同时获取数据,并取最快的那份数据进行渲染。

但在此“数据竞速”场景下,由于我们需要对两个数据源返回的数据进行合法性校验,因此 promise 自带的方法不能完全适用,因为他无法满足我们“带数据合法性校验的竞速请求”的场景。这里我们探讨并给出一种实现。

场景解释

所谓“带数据合法性校验的竞速请求”是指的:我要同时发出 2 个请求,且希望尽快拿到最早返回的“符合数据合法性校验要求”的请求结果。

举例来说,假如我打开页面后,希望拿到一个数据,这份数据同时放到了“cdn 服务器地址”和“服务端后台地址”,于是我希望打开页面后立刻向 2 个地址发出请求并拿到最早的那个符合要求的结果进行展示。

这里所谓的“最早的符合要求”,是指的要满足 2 点要求:

  1. 它是尽可能早返回的那个数据,因为页面需要尽早拿到合法数据进行渲染。
  2. 返回的这个数据需要满足“数据合法性校验函数”。因为最先拿到的远程的数据不一定准确,所以我们要求必须返回那个“既合法,又早”的数据。

Promise 自带的方法

Promise 自带的有 Promise.race, Promise.all, Promise.any 等,但是却无法满足该场景。

  • race 函数,确实可以实现竞速。但是它在任何一个请求最早 settle 的时候就会立刻交付结果。这个交付的结果可能是“失败的”结果。而我们期望如果最早的那个请求失败了,那应该向外交付后面那个结果。毕竟在页面渲染场景下:即使渲染慢一点,也不要 fail。
  • all 函数。它需要等到所有 promise 全部敲定后,才会交付结果。这无法满足我们“尽快”的要求。
  • any 函数。这是较新版本浏览器才支持的功能,它可以在任意一个 promise 能达到 resolve 的情况下则交付结果,否则会等其他 promise,直到有一个 resolve 或者全部都无法完成才交付结果。这个看起来能满足我们“尽快且成功的”要求,但是无法满足“尽快+合法”的要求。在我们场景下,还需要 any 函数中帮我们做一下数据合法性校验,若数据不合法,也应当继续等待其他请求。

自己实现第一版

思路

俩请求同时发出,但是响应的时候,要先校验是否合法,合法则认为成功,成功就立刻 resolve 尽量让外面赶紧使用这份成功数据; 若失败则要看看是否还要等,没必要等的情况下才 reject—也就是只有最后一个 promise 才可能 reject。

总结来看:

  1. 成功后,不管三七二十一,直接 resolve 即可。这里利用 promise 状态不变的特性,自然可以避免多次 resolve 发生。
  2. 失败后,需要看“是否需要继续等待”再决定 reject。所谓“是否有必要继续等”,是需要看看是否还有可以继续等待的其他 promise。实现上,可以靠计数来实现。

具体代码

1
2
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++; // 本来这个 finalCount++我想写到上面 2 个 promise 的 finally 函数里来着,但是后来发现 finally 也是个 promise,他会让 2 个请求的 finalCount 全部加完之后,才会再走到各自的 then里面,会出现计数不准的问题。因此我们改成统一在这里同步处理。
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 个独立的调用上下文,但我们代码就可以只写一次。

1
2
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
// 这里借助一个循环也可以避免重复代码。
// 然后 then 处理逻辑就不需要搬到单独函数里了,因为这里毕竟不重复嘛。于是我就直接写到 then里。
function raceForTheBestRequest(one, two, checkFunc) {
return new Promise((resolve, reject) => {
const promiseArr = [one, two];
const settledPromise = []; // 这里用一个数组存储完成的 promise,最后靠数组长度判断完成了几个。比我用计数器高级一些,更容易理解一些!
promiseArr.forEach((p) => {
p()
.then((res) => {
settledPromise.push(p);
if (checkFunc(res)) {
resolve(res);
} else {
console.log("数据校验不通过,扔出");
// 注意这里必须同步判断 setledPromise.length 来处理错误,于是交给一个同步函数来处理。
_onThrowError(new Error("数据格式不符合要求"));
}
})
.catch((err) => {
console.log("等到一个错误", err);
settledPromise.push(p);
// 注意这里同样必须同步判断 setledPromise.length 来处理错误,于是交给一个同步函数来处理。
_onThrowError(err);
});
});
function _onThrowError(err) {
if (settledPromise.length === promiseArr.length) {
reject(err);
}
}
});
}

测试用例

1
2
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 时间,多次尝试执行上述代码来进行测试。确保符合预期即可。