今天在群里聊天,忽然有人放出了一道面试题。通过群里一番讨论,最终解题思路慢慢完善起来,我这里就整理一下群内解题的思路。面试
该题定义了一个同步函数对传入的数组进行遍历乘二操做,同时每执行一次就会给 executeCount
累加。最终咱们须要实现一个 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) }
拿到题目的第一时间,我就想到了抖机灵的方法。直接面向用例编程,执行完以后重置下 executeCount
就行了。数组
const batcher = f => { return nums => { try { return f(nums) } finally { executeCount = 1 } } }
固然除非你不在意此次面试,不然通常不建议你用这种抖机灵的方法回答面试官(不要问我为何知道)。因为 executeCount
的值和 fn()
函数的调用次数呈正相关,因此这道理也就换成了咱们须要实现 batcher()
方法返回新的包装函数,该函数会被调用屡次,但最终只会执行一次 fn()
函数。浏览器
因为题干中使用了 Promise.all()
,咱们天然而然想到使用异步去解决。也就是每次调用的时候会把因此的传参存下来,直到最后的时候再执行 fn()
返回对应的结果。问题在于何时触发开始执行呢?天然而然咱们想到了相似 debounce
的方式使用 setTimeout
增长延迟时间。异步
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)); }; };
这里的难点在于预先定义了一个 Promise 在 100ms 以后才会 resolve。返回的函数本质只是将参数推入到 nums
数组中,待 100ms 后触发 resolve 返回统一执行 fn()
后的结果并获取对应于当前调用的结果片断。async
后来有群友反馈,实际上不用定义 100ms 直接 0ms 也是能够的。因为 setTimeout
是在 UI 渲染结束以后才会执行的宏任务,因此理论上来讲 setTimeout()
的最小间隔值没法设置为 0。它的最小值和浏览器的刷新频率有关系,根据 MDN 描述,它的最小值通常为 4ms。因此理论上它设置 0ms 和 100ms 效果是差很少的,都相似于 debounce
的效果。函数
那么如何能实现延迟 0ms 执行呢?咱们知道除了宏任务以外 JS 还有微任务,微任务队列是在 JS 主线程执行完成以后当即执行的事件队列。Promise 的回调就会存储在微任务队列中。因而咱们将 setTimeout
修改为了 Promise.resolve()
,最终发现也是能够实现一样的效果。oop
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)); }; };
因为 Promise 的微任务队列效果将 _ => f(nums)
推入微任务队列,待主线程的三次 batcherFn()
调用都执行完成以后才会执行。以后 p
的状态变为 fulfilled
后继续完成最终 slice
的操做。post
最终分析下来,其实这道理的本质就是要经过某些方法将 fn()
函数的执行后置到主线程执行完毕,至因而使用宏任务仍是微任务队列,就看具体的需求了。除了 setTimeout()
以外,还有 setInterval()
, requestAnimationFrame()
都是宏任务队列。而微任务队列里除了有 Promise
以外,还有 MutationObserver
。关于宏任务和微任务队列相关的,感兴趣的能够看看《微任务、宏任务与Event-Loop》这篇文章。线程