Implementing a Batcher Function to Ensure a Single Execution of a Synchronous Array‑Doubling Function
This article explains how to wrap a synchronous function that doubles array elements so that multiple calls return the expected doubled results while the underlying function is executed only once, using clever tricks, setTimeout‑based debouncing, and Promise‑based micro‑task scheduling.
One day in a group chat, an interview question was posted; after discussion the solution ideas were refined, and this article consolidates the group's approach.
The problem defines a synchronous function that iterates over an input array, multiplies each element by two, and increments executeCount on each call. The goal is to implement a batcher function that wraps this synchronous function so that every invocation still returns the expected doubled array, but executeCount is incremented only once.
let executeCount = 0
const fn = nums => {
executeCount++
return nums.map(x => x * 2)
}
const batcher = f => {
// TODO: implement 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 cases
assert(r1).tobe([2,4,6])
assert(r2).tobe([8,10])
assert(r3).tobe([14,16,18])
assert(executeCount).tobe(1)
}Clever Trick Solution
The first idea is to directly reset executeCount after each call, effectively forcing the count to stay at 1 regardless of how many times the wrapper is invoked.
const batcher = f => {
return nums => {
try { return f(nums) } finally { executeCount = 1 }
}
}Although this works for the interview, it is generally discouraged because the value of executeCount is tightly coupled with the number of times fn() is called, and the wrapper would still invoke the original function multiple times.
setTimeout Solution
Since the problem uses Promise.all() , an asynchronous approach is natural. Each call stores its arguments, and execution is deferred until a later moment using a debounce‑like setTimeout delay.
const batcher = f => {
let nums = [];
const p = new Promise(resolve => setTimeout(_ => resolve(f(nums)), 100));
return arr => {
let start = nums.length;
nums = nums.concat(arr);
let end = nums.length;
return p.then(ret => ret.slice(start, end));
};
};The key is that a single Promise is created that resolves after 100 ms; each call merely pushes its arguments into the nums array, and when the timeout fires the original function runs once on the accumulated arguments, with each caller receiving the slice that corresponds to its own input.
In practice the delay can be 0 ms because setTimeout executes after the UI rendering macro‑task, and browsers enforce a minimum timeout (typically ~4 ms), which still provides a debounce‑like effect.
Promise Solution
To avoid the macro‑task delay, the same idea can be implemented with a micro‑task using Promise.resolve() . The callback is queued in the micro‑task queue and runs immediately after the current call stack finishes.
const batcher = f => {
let nums = [];
const p = Promise.resolve().then(_ => f(nums));
return arr => {
let start = nums.length;
nums = nums.concat(arr);
let end = nums.length;
return p.then(ret => ret.slice(start, end));
};
};Because the micro‑task runs after the three batcherFn() calls complete, the original function is executed only once, and each caller receives the appropriate slice of the result.
A later improvement adds a guard so that the Promise is created only on the first call, and after the result is sliced the internal state is cleared for potential reuse.
const batcher = (f) => {
let nums = [];
let p;
return (arr) => {
if (!p) { p = Promise.resolve().then(_ => f(nums)); }
const start = nums.length;
nums = nums.concat(arr);
const end = nums.length;
return p.then(ret => {
nums = [];
p = null;
return ret.slice(start, end);
});
};
};Final Remarks
The essence of the problem is to postpone the execution of fn() until the main thread has finished processing all calls, using either macro‑tasks ( setTimeout , setInterval , requestAnimationFrame ) or micro‑tasks ( Promise , MutationObserver ). Readers interested in the event loop can refer to articles on micro‑tasks, macro‑tasks, and the JavaScript event loop.
References:
https://github.com/dwqs/blog/issues/70#
https://github.com/ant-design/ant-design/issues/21022
https://github.com/ant-design/ant-design/issues/20339
ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.