今天在羣裏聊天,忽然有人放出了一道面試題。通過羣裏一番討論,最終解題思路慢慢完善起來,我這裏就整理一下羣內解題的思路。面試
該題定義了一個同步函數對傳入的數組進行遍歷乘二操做,同時每執行一次就會給 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》這篇文章。線程