Today, I was chatting in the group, and suddenly someone released an interview question. After some discussion in the group, the final solution ideas slowly improve, I will sort out the group solution ideas here.
This problem defines a synchronization function that iterates through an array, multiplying by two, and incrementing executeCount each time it is executed. Ultimately, we need to implement a Batcher function that wraps the synchronization function in such a way that each call still returns twice the expected result, while ensuring that executeCount is executed 1 times.
let executeCount = 0
const fn = nums= > {
executeCount++
return nums.map(x= > x * 2)}const batcher = f= > {
// Todo implements batcher functions
}
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]]);// Satisfy the following test cases
assert(r1).tobe([2.4.6])
assert(r2).tobe([8.10])
assert(r3).tobe([14.16.18])
assert(executeCount).tobe(1)}Copy the code
Clever solution
The first time I got the topic, I thought of a clever way to shake. Program directly to the use case, and then reset executeCount.
const batcher = f= > {
return nums= > {
try { return f(nums) } finally { executeCount = 1}}}Copy the code
Unless, of course, you don’t care about the interview, this clever way of answering is generally not recommended (don’t ask me how I know). Since the value of executeCount is positively correlated with the number of times the fn() function is called, we need to implement the batcher() method to return a new wrapper function that is called multiple times, but ultimately only executes the FN () function once.
SetTimeout method
Because of the use of promise.all () in the problem stem, it is natural to use asynchronous solutions. Fn () returns the result of each call. The question is when does the trigger start executing? Naturally we thought of something like Debounce using setTimeout to increase the delay time.
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));
};
};
Copy the code
The difficulty here is that a Promise is predefined to resolve after 100ms. The function that is returned essentially just pushes the arguments into the NUMS array and, after 100ms, triggers resolve to return the result of the unified execution of fn() and retrieves the result fragment corresponding to the current call.
Later, there was feedback from the group, in fact, it is not necessary to define 100ms directly 0ms is also ok. Since setTimeout is a macro task that does not execute until after UI rendering is complete, theoretically the minimum interval of setTimeout() cannot be set to 0. Its minimum value is related to the refresh frequency of the browser, and according to the MDN description, its minimum value is generally 4ms. So it is theoretically equivalent to setting 0ms and 100ms, both of which are similar to debounce.
Promise solution
So how can we implement delayed 0ms execution? We know that in addition to macro tasks, JS also has microtasks. Microtask queues are the queues of events that are executed immediately after the main JS thread completes execution. The Promise callback is stored in the microtask queue. We changed setTimeout to promise.resolve () and found that the same effect could be achieved.
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));
};
};
Copy the code
Because Promise’s microtask queue effect pushes _ => f(NUMs) into the microtask queue, it will not be executed until all three batcherFn() calls to the main thread have been executed. Later, P will gradually complete the final slice operation after the depressing.
2020-03-17: Thanks to @kricsleo for pointing out the problem with multiple calls due to side effects and providing an optimized version.
const batcher = (f) = > {
let nums = [];
let p;
return (nums) = > {
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);
});
};
};
Copy the code
Afterword.
Finally, the essence of this principle is to use some method to put the fn() function after the main thread is finished, as to whether to use macro task or micro task queue, depending on the specific requirements. In addition to setTimeout(), setInterval() and requestAnimationFrame() are macro task queues. In addition to promises, there are mutationObservers in the microtask queue. For more information on macro tasks and microtask queues, see the article microtasks, macro tasks, and Event-loops.