之前看到一道有关批处理的题目, 感觉挺不错, 但分析的过程我觉得可以更详细一些, 在这里分享出我的分析过程
题干
实现一个 batcher 函数, 使用其对同步函数包装后, 实现每次调用依旧返回预期的结果, 同时还需要保证 executeCount 执行次数为 1
let executeCount = 0
const fn = nums => {
executeCount++
return nums.map(x => x * 2)
}
const batcher = f => {
// todo 实现 batcher 函数
}
const batchedFn = batcher(fn);
const main = async () => {
const [r1, r2, r3] = await Promise.all([
batchedFn([1,2,3]),
batchedFn([4,5]),
batchedFn([7,8,9])
]);
//满足以下 test case
assert(r1).tobe([2, 4, 6])
assert(r2).tobe([8, 10])
assert(r3).tobe([14, 16, 18])
assert(executeCount).tobe(1)
}
思路
我们可以将整个题干代码分为两部分
main部分- 可用资源, 如
executeCount,fn等
我们将通过分析这两部分为编写 batcher 函数提供思路
main
首先我们来查看题干代码的 main 部分
const main = async () => {
const [r1, r2, r3] = await Promise.all([
batchedFn([1,2,3]),
batchedFn([4,5]),
batchedFn([7,8,9])
]);
//满足以下 test case
assert(r1).tobe([2, 4, 6])
assert(r2).tobe([8, 10])
assert(r3).tobe([14, 16, 18])
assert(executeCount).tobe(1)
}
从上述代码中我们可以得到一些信息:
- 出现
async,Promise等, 可知本题中涉及到异步 batchedFn函数共被调用了三次- 从 test case 可以看出,
batchedFn函数的作用是将数组中的数据翻倍
可见, batchedFn 函数就是 main 的关键, 接下来我们就以它为线索分析题干代码提供的资源
资源
batchedFn
首先我们在可用资源中找到 batchedFn 函数
const batchedFn = batcher(fn);
从上述代码中我们可以知道, batchedFn 函数是通过以 fn 为参数调用需要我们编写的 batcher 函数获得, 即 batcher 函数的返回值为一个函数
由此我们可以知道, batcher 函数是一个函数工厂, 且以 fn 为参数所生产函数的功能为: 以数组为参数, 将传入数组包含的所有元素翻倍
而涉及到函数工厂, 闭包的使用往往是需要考虑的
我们可以将已得到的信息写入 batcher 中
const batcher = f => {
// 闭包内数据
// 返回函数
return arr => {}
}
fn
在获取 batchedFn 时, 我们使用到了 fn 作为参数,
接下来我们来看一看这部分
let executeCount = 0
const fn = nums => {
executeCount++
return nums.map(x => x * 2)
}
fn 的作用是将传入数组 nums 中的每一个数据翻倍, 同时会用 executeCount 记录 fn 的使用次数. 由此我们可以知道: batchedFn 的功能由 fn 带来
但与此同时, 在 main 中我们已经了解到: batchedFn 被调用了三次; 而根据题目要求, fn 只能够被调用一次. 可见 fn 的执行并不和 batchedFn 同步
因此, 我们可以将 fn, 也就是 batcher 中的 f 处理为异步操作, 将其放入微任务队列中排队; 而在本次执行栈从有到空的过程中, 每次调用 batcherFn 都起到收集 fn 需要处理数据的作用, 从而当 fn 进入执行栈时, 能实现 fn 一次性处理多次调用 batcherFn 收集到的数据
需求
这时, 我们就获得了 batcher 所生产函数的两项需求:
- 将
f处理为异步操作 - 收集每次调用传入的数据
这里需要注意需求的细节: 无论在本次执行栈中调用多少次 batcher 所生产的函数, f 只调用一次, 传入的数据将汇总于一处, 即生产的函数本身能够记录, 这正是闭包的用武之地
综上, 我们可以对 batcher 进行如下处理:
const batcher = f => {
// 用于存储生产的函数被调用时将获得的数据
let nums = [];
// 记录将 f 推入微任务队列中排队的 Promise
let p;
// 返回函数
return arr => {
// 判断是否已经将 f 推入微任务队列中排队
!p && p = Promise.resolve().then(_ => f(nums));
// 记录传入数据所在位置, 以便从结果中裁切
const start = nums.length;
// 存储传入的数据
nums = nums.concat(arr);
const end = nums.length;
// 将数据翻倍的操作在调用该函数时并未完成
// 因此返回一个 Promise
return p.then(finalNums => {
// f 执行完成后除了将各次传入数据对应结果返回, 还需要将函数的记录清空
nums = [];
p = undefined;
return finalNums.slice(start, end);
});
}
}
总结
一道批处理题目同时涉及到异步和闭包, 对我而言, 仅仅是根据答案整理思路都感觉有些困难了, 而且这只是我自己整理的思路, 如果大家有更好的想法或是我的思路存在问题, 欢迎大家多多交流:)