一道面試題讓你更加了解事件隊列

今天在羣裏聊天,忽然有人放出了一道面試題。通過羣裏一番討論,最終解題思路慢慢完善起來,我這裏就整理一下羣內解題的思路。面試

該題定義了一個同步函數對傳入的數組進行遍歷乘二操做,同時每執行一次就會給 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() 函數。瀏覽器

setTimeout 解法

因爲題幹中使用了 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 的效果。函數

Promise 解法

那麼如何能實現延遲 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》這篇文章。線程

相關文章
相關標籤/搜索