Frontend Development 8 min read

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.

ByteFE
ByteFE
ByteFE
Implementing a Batcher Function to Ensure a Single Execution of a Synchronous Array‑Doubling Function

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

javascriptmacrotaskMicrotaskPromisedebouncebatcher
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.