共计 5350 个字符,预计需要花费 14 分钟才能阅读完成。
简略优雅的 JavaScript 代码片段文章系列:
简略优雅的 JavaScript 代码片段(一):异步控制
简略优雅的 JavaScript 代码片段(二):流控和重试
简略优雅的 JavaScript 代码片段(三):合并申请,成批收回
场景阐明
后端提供的接口具备批量查问能力(比方,同时查问 10 个资源的详情信息),然而调用者每次只须要申请一个资源的详情信息。
比方:
import React from "react";
const List = ({ids}: {ids: string[] }) => {
return (
<div>
{ids.map((id) => (<ResourceDetail key={id} id={id} />
))}
</div>
);
};
const ResourceDetail = ({id}: {id: string}) => {
// 在这个组件申请资源详情并渲染...
// 留神在这个组件中,你只关怀这一个资源的信息,你不再具备”整个列表“的数据视角。};
// 后端接口反对同时申请多个资源的信息
declare function api(ids: string[]): Promise<Record<string, {details: any}>>;
这个时候,个别只有 2 个计划:
-
间接应用批量接口来申请单个资源的信息。在 ResourceDetail 组件中,间接调用 api 来申请以后资源的信息:
api([id])
,只传入以后资源 ID。- 益处是简略间接,易于了解;
- 害处是 没有将接口自身的批量申请能力利用起来,收回过多申请,导致接口流控限度或效率低下。
-
将申请逻辑晋升至更高的组件档次,在具备”整个列表“的数据视角的时候进行批量申请。在下层的 List 组件中,批量申请多个资源的信息:
api([id1, id2, ...])
,而后将后果传递给子组件进行渲染。- 益处是将接口自身的批量申请能力利用起来;
- 害处是 代码耦合严密 (ResourceDetail 依赖父组件帮它申请资源信息); 代码逻辑设计投合技术因素,不合乎直觉 ; 造成 List 组件职责扩充、逻辑简单(比方接口每次最多只能反对 10 个资源批量申请,因而你要将列表分为 10 个一组,别离申请)。
有没有两败俱伤计划呢?既能让组件职责简略清晰,又能将接口自身的批量申请能力利用起来?
解决方案
这篇文章介绍一个工具函数,将一个【批量申请】的接口转换成【单个申请】的接口:wrapBatchProcess
。示例用法:
// 先应用 wrapBatchProcess 工具将 api 转换成单个申请的接口
// const wrappedAPI: (input: string) => Promise<{details: any}>
const wrappedAPI = wrapBatchProcess<string, {details: any}>(async (inputs, onResult, onError) => {
// 工具函数将「会集」成一大批申请,调用你提供的这个回调函数
// 因而你就能够在这个回调中同时解决一大批申请了!const result = await api(inputs.map(({ input}) => input))
Object.keys(result).forEach((key) => {
// 申请到后果当前,将后果提供给工具函数,而后工具函数就会将后果提供给调用者
onResult(result[key], key)
})
},
// getKey 函数,在这个场景下比较简单。实现中的代码正文有解释
(input) => input
)
// 调用 wrappedAPI,每次只须要传入一个申请。工具会主动合并申请,通过 api 来批量收回。// 后果达到当前,wrappedAPI 会将【对应于这个申请的后果】返回回来
wrappedAPI(id) // Promise<{details: any}>
wrappedAPI
实质上是一个申请的缓冲队列(buffer),尽管它一个一个地承受申请,然而它不会立刻将申请收回,而是期待一段时间(debounce),将申请累积起来,而后将累积起来的申请成批收回。
wrapBatchProcess
就是一个创立 wrappedAPI
的工具函数。用户传入一个函数来定义【如何解决一批申请】,而后它给用户返回wrappedAPI
,一个一个地接管申请。
劣势
封装后的 api 非常简单:(input) => Promise<TheOutput>
。因而 api 使用者(ResourceDetail)只须要 简略地调用wrappedAPI
,就能够拿到它想要的数据!
使用者(ResourceDetail)不须要关怀【申请是如何合并收回的】,简单的申请合并逻辑被抽离到了【wrapBatchProcess 的调用代码】中,这些代码齐全能够放在其余的文件中(关注点拆散)。不会净化 ResourceDetail 和 List 组件。
接口自身的批量申请能力被充分利用,晋升申请效率,缩小申请数量。
实现代码
import debounce from "lodash.debounce";
/**
*「会集」解决申请,成批处理。* 将「批量申请」接口封装成「一一申请」的接口。* 让使用者享受「一一解决」的简略性,同时在底层通过「批量申请」来取得效率晋升。* 使用者只须要关怀以后 input 的解决,而不须要关怀这个 input 是如何与其余 input 一起成批申请的(关注点拆散)。*/
export function wrapBatchProcess<InputItem, OutputItem>(
// 之所以通过 callback 的形式来返回数据,是因为要反对分批屡次返回(拿到一批响应就立即返回给调用者),而不是期待所有后果达到当前再全副一次性返回
fn: (inputs: { input: InputItem; key: string}[],
/**
* 返回后果的时候,须要返回 key,与 input 的 key 对应,这样咱们能力晓得每个 output 对应于哪个 input
*/
onResult: (output: OutputItem, key: string) => void,
onError: (error: any, key: string) => void
) => void,
// input 可能是一个简单对象,而 wrapBatchProcess 须要一个 string 来标识一个申请
getKey: (input: InputItem) => string
/** 封装后函数的使用者不须要理解 key 的概念 */
): (input: InputItem) => Promise<OutputItem> {let buffer: Map<string, BufferItem> = new Map();
const check = debounce(() => {if (buffer.size === 0) return;
// 将整个 buffer 作为一批,发出请求,并清空 buffer
const batch = new Map(buffer);
buffer = new Map();
const inputs = Array.from(batch.values()).map((item) => ({
key: item.key,
input: item.input,
}));
fn(
inputs,
(output, key) => {const item = batch.get(key);
if (!item) return;
item.resolve(output);
batch.delete(key);
},
(error, key) => {const item = batch.get(key);
if (!item) return;
item.reject(error);
batch.delete(key);
}
);
},
// 期待若干毫秒作为一个申请收集窗口,而后将收集到的所有申请作为一批收回
50,
{
leading: false,
trailing: true,
// 防止一直有申请到来,导致 debounce 始终无奈被调用,这个参数可调
maxWait: 200,
}
);
function schedule(input: InputItem) {const key = getKey(input);
// 如果曾经有雷同的 input 在 buffer 中,则不反复调度它,而是与前一个 input 共享同一个后果
const existBufferItem = buffer.get(key);
if (existBufferItem) return existBufferItem.promise;
// 将 input 信息退出 buffer 中,筹备调度
const bufferItem: BufferItem = {
input,
key,
...createControllablePromise<OutputItem>(),};
buffer.set(key, bufferItem);
check();
return bufferItem.promise;
}
return schedule;
type BufferItem = {
input: InputItem;
key: string;
promise: Promise<OutputItem>;
resolve: (ret: OutputItem) => void;
reject: (error: any) => void;
};
}
function createControllablePromise<T>(): {
promise: Promise<T>;
resolve: (ret: T) => void;
reject: (error: any) => void;
} {let result: any = {};
result.promise = new Promise<T>((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
}
接口批量能力无限
很多时候,尽管后端接口反对在一个申请中同时查问多个资源 ID 的信息,然而这个反对的数量也会有一个下限。比方每个申请最多只反对查问 10 个资源 ID。
那么咱们在调用 api 之前也须要做对应的适配:
const wrappedAPI = wrapBatchProcess<string, {details: any}>(async (inputs, onResult, onError) => {
// 因为 api 接口每次最多只能反对 10 个资源查问,而 inputs 数组可能很多
// 因而要先将 inputs 切分成多个组,每 10 个资源一组,确保 api 能承受
const groups: string[][] = splitArrayIntoGroups(inputs.map(({ input}) => input),
10
);
groups.forEach(async (group: string[]) => {const result = await api(group);
Object.keys(result).forEach((key) => {onResult(result[key], key);
});
});
},
(input) => input
);
function splitArrayIntoGroups<T>(array: T[], groupSize: number): T[][] {const result: T[][] = [];
for (let i = 0; i < array.length; i += groupSize) {const group = array.slice(i, i + groupSize);
result.push(group);
}
return result;
}
可组合性
这篇文章介绍的工具函数能够与简略优雅的 JavaScript 代码片段(二):流控和重试介绍的工具函数组合应用。
比方,在咱们后面介绍的”接口批量能力无限“的例子中,如果一次性到来了 110 个资源 ID,那么在成批收回的时候,就会同时收回 11 个申请(每个申请蕴含 10 个资源 ID 查问)。
然而咱们的 api
有10 次 / 秒
的流控限度。如何防止超出这个限度?
你能够利用上篇文章介绍的 wrapFlowControl
,将api
加强成apiWithFlowControl
(适配流控的申请办法):
const apiWithFlowControl = wrapFlowControl(api, 10);
const wrappedAPI = wrapBatchProcess<string, {details: any}>(async (inputs, onResult, onError) => {const groups: string[][] = splitArrayIntoGroups(inputs.map(({ input}) => input),
10
);
groups.forEach(async (group: string[]) => {
// 这一行替换成 apiWithFlowControl 即可!const result = await apiWithFlowControl(group);
Object.keys(result).forEach((key) => {onResult(result[key], key);
});
});
},
(input) => input
);
这样,你取得了 wrapBatchProcess 的所有益处,同时还能确保它会 在流控容许的范畴内调度 api!