Linux中國

寫一個 JavaScript 框架:比 setTimeout 更棒的定時執行

這個系列包含以下幾個章節:

  1. 項目結構
  2. 定時執行 (當前章節)
  3. 沙箱代碼評估
  4. 數據綁定介紹
  5. 數據綁定與 ES6 代理
  6. 自定義元素
  7. 客戶端路由

非同步代碼執行

你可能比較熟悉 Promiseprocess.nextTick()setTimeout(),或許還有 requestAnimationFrame() 這些非同步執行代碼的方式。它們內部都使用了事件循環,但是它們在精確計時方面有一些不同。

在這一章里,我將解釋它們之間的不同,然後給大家演示怎樣在一個類似 NX 這樣的先進框架裡面實現一個定時系統。不用我們重新做一個,我們將使用原生的事件循環來達到我們的目的。

事件循環

事件循環甚至沒有在 ES6 規範里提到。JavaScript 自身只有任務(Job)和任務隊列(job queue)。更加複雜的事件循環是在 NodeJS 和 HTML5 規範里分別定義的,因為這篇是針對前端的,我會在詳細說明後者。

事件循環可以被看做某個條件的循環。它不停的尋找新的任務來運行。這個循環中的一次迭代叫做一個滴答(tick)。在一次滴答期間執行的代碼稱為一次任務(task)。

while (eventLoop.waitForTask()) {  
  eventLoop.processNextTask()
}

任務是同步代碼,它可以在循環中調度其它任務。一個簡單的調用新任務的方式是 setTimeout(taskFn)。不管怎樣, 任務可能有很多來源,比如用戶事件、網路或者 DOM 操作。

任務隊列

更複雜一些的是,事件循環可以有多個任務隊列。這裡有兩個約束條件,相同任務源的事件必須在相同的隊列,以及任務必須按插入的順序進行處理。除此之外,瀏覽器可以做任何它想做的事情。例如,它可以決定接下來處理哪個任務隊列。

while (eventLoop.waitForTask()) {  
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }
}

用這個模型,我們不能精確的控制定時。如果用 setTimeout()瀏覽器可能決定先運行完其它幾個隊列才運行我們的隊列。

微任務隊列

幸運的是,事件循環還提供了一個叫做微任務(microtask)隊列的單一隊列。當前任務結束的時候,微任務隊列會清空每個滴答里的任務。

while (eventLoop.waitForTask()) {  
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }

  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }
}

最簡單的調用微任務的方法是 Promise.resolve().then(microtaskFn)。微任務按照插入順序進行處理,並且由於僅存在一個微任務隊列,瀏覽器不會把時間弄亂了。

此外,微任務可以調度新的微任務,它將插入到同一個隊列,並在同一個滴答內處理。

繪製 Rendering

最後是 繪製 Rendering 調度,不同於事件處理和分解,繪製並不是在單獨的後台任務完成的。它是一個可以運行在每個循環滴答結束時的演算法。

在這裡瀏覽器又有了許多自由:它可能在每個任務以後繪製,但是它也可能在好幾百個任務都執行了以後也不繪製。

幸運的是,我們有 requestAnimationFrame(),它在下一個繪製之前執行傳遞的函數。我們最終的事件模型像這樣:

while (eventLoop.waitForTask()) {  
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }

  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }

  if (shouldRender()) {
    applyScrollResizeAndCSS()
    runAnimationFrames()
    render()
  }
}

現在用我們所知道知識來創建定時系統!

利用事件循環

和大多數現代框架一樣,NX 也是基於 DOM 操作和數據綁定的。批量操作和非同步執行以取得更好的性能表現。基於以上理由我們用 PromisesMutationObserversrequestAnimationFrame()

我們所期望的定時器是這樣的:

  1. 代碼來自於開發者
  2. 數據綁定和 DOM 操作由 NX 來執行
  3. 開發者定義事件鉤子
  4. 瀏覽器進行繪製

步驟 1

NX 寄存器對象基於 ES6 代理 以及 DOM 變動基於MutationObserver (變動觀測器)同步運行(下一節詳細介紹)。 它作為一個微任務延遲直到步驟 2 執行以後才做出反應。這個延遲已經在 Promise.resolve().then(reaction) 進行了對象轉換,並且它將通過變動觀測器自動運行。

步驟 2

來自開發者的代碼(任務)運行完成。微任務由 NX 開始執行所註冊。 因為它們是微任務,所以按序執行。注意,我們仍然在同一個滴答循環中。

步驟 3

開發者通過 requestAnimationFrame(hook) 通知 NX 運行鉤子。這可能在滴答循環後發生。重要的是,鉤子運行在下一次繪製之前和所有數據操作之後,並且 DOM 和 CSS 改變都已經完成。

步驟 4

瀏覽器繪製下一個視圖。這也有可能發生在滴答循環之後,但是絕對不會發生在一個滴答的步驟 3 之前。

牢記在心裡的事情

我們在原生的事件循環之上實現了一個簡單而有效的定時系統。理論上講它運行的很好,但是還是很脆弱,一個輕微的錯誤可能會導致很嚴重的 BUG。

在一個複雜的系統當中,最重要的就是建立一定的規則並在以後保持它們。在 NX 中有以下規則:

  1. 永遠不用 setTimeout(fn, 0) 來進行內部操作
  2. 用相同的方法來註冊微任務
  3. 微任務僅供內部操作
  4. 不要干預開發者鉤子運行時間

規則 1 和 2

數據反射和 DOM 操作將按照操作順序執行。這樣只要不混合就可以很好的延遲它們的執行。混合執行會出現莫名其妙的問題。

setTimeout(fn, 0) 的行為完全不可預測。使用不同的方法註冊微任務也會發生混亂。例如,下面的例子中 microtask2 不會正確地在 microtask1 之前運行。

Promise.resolve().then().then(microtask1)  
Promise.resolve().then(microtask2) 

規則 3 和 4

分離開發者的代碼執行和內部操作的時間窗口是非常重要的。混合這兩種行為會導致不可預測的事情發生,並且它會需要開發者了解框架內部。我想很多前台開發者已經有過類似經歷。

結論

如果你對 NX 框架感興趣,可以參觀我們的主頁。還可以在 GIT 上找到我們的源代碼

在下一節我們再見,我們將討論 沙盒化代碼執行

你也可以給我們留言。

via: https://blog.risingstack.com/writing-a-javascript-framework-execution-timing-beyond-settimeout/

作者:Bertalan Miklos 譯者:kokialoves 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國