本系列一共七章,Github 地址請查閱這裏,原文地址請查閱這裏。javascript
本章會闡述瀏覽器端執行異步代碼的各類不一樣方法。你將會了解到關於事件循環和定時技術好比 setTimeout 和 Promises 之間的差異。html
大多數人可能對諸如 Promise
,process.nextTick()
,setTimeout
,或許還有 requestAnimationFrame
的異步執行代碼比較熟悉。它們內部都是使用的事件循環,可是在精準的定時上面,他們表現得很不同。前端
本文,將會闡述其中的不一樣,而後讓你明白如何去實現一個現代框架好比 NX 所須要的定時系統。與其重複造輪子,咱們將會使用原生的事件循環來實現咱們的目標。html5
事件循環在 ES6 規範 中並無的說起。JavaScript 自己只有任務和任務隊列的概念。更復雜的事件循環的概念分別定義在 NodeJS 和 HTML5 規範中。由於此係列是講的前端,因此在這裏我將會闡述後者。java
事件循環被稱爲一個循環,是有一個緣由的。它無限循環並尋找新任務來執行。該循環的單次遍歷叫作一個 tick。在tick 期間執行的代碼叫一個任務(task)。react
while (eventLoop.waitForTask()) {
eventLoop.processNextTask()
}
複製代碼
任務是指能夠在循環中安排其餘任務的同步代碼片斷。調度新任務的一種簡單的編程方法是 setTimeout(taskFn)
。git
然而,任務可能來自其它幾種來源,好比用戶事件,網絡請求或者 DOM 操做。es6
更復雜的就是,事件循環能夠擁有多個任務隊列。惟一的兩個限制便是來自同一個任務源的事件必須屬於同一個任務隊列,而且每一個隊列中的全部的任務必須按插入的順序執行。除了這些,用戶代理能夠隨意作任何操做。例如,它能夠決定下一個執行的任務隊列。github
While (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
}
複製代碼
依據這個模型,咱們就失去了對時間的精確控制。在執行咱們用 setTimeout()
規劃的任務以前,瀏覽器可能決定先徹底處理其它的幾個任務隊列。web
幸運的是,事件循環還有一個單一隊列叫作微任務隊列。在每一個 tick 之中,每當當前任務執行完畢以後,微任務隊列都被徹底清空。
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)
。微任務會按插入的順序執行,由於只有一個微任務隊列,因此用戶代理此次不能干擾咱們。
另外,微服務能夠設置新的微服務插入到同一個微服務隊列中,而且在同一個 tick 中執行。
最後一個須要注意的是渲染的時間表。不像事件處理或者解析,渲染不是由獨立的後臺任務來完成的。這是一種算法,能夠運行在每個循環 tick 結束。
用戶代理又有不少自由的選擇:它可能在每一個任務以後渲染,可是它可能會決定執行成百上千的任務而不去渲染。
幸運的是有 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 操做和數據綁定。它批量操做並異步執行它們以提高性能。爲了讓這些操做按正確地時序執行,它依賴於 Promises
,MutationObservers
和 requestAnimationFrame()
。
預想的定時系統以下:
NX 同時使用 ES6 Proxies 來註冊一個對象變更和使用 MutationObserver 來註冊 DOM 變更(下一章將更多介紹這些內容)。它會延遲響應,做爲微任務的第二步以提高性能。延遲響應對象變化是使用 Promise.resolve().then(reaction)
來實現的,而後由 MutationObserver 來自動處理,由於 MutationObserver 內部使用微任務。
開發者的代碼(任務)運行結束。由 NX 註冊的微任務響應開始執行。微任務是按順序執行的。注意此時咱們仍然在同一個循環 tick 中。
NX 使用 requestAnimationFrame(hook)
來運行由開發者傳過來的鉤子。這也許會發生在以後的循環 tick 之中。重要的是,這裏的鉤子會在下一次渲染以前和全部的數據,DOM 和 CSS 的更改都執行以後運行。
瀏覽器渲染接下來的視圖。這也許會發生在以後的循環 tick 之中,可是它毫不可能發生在一個 tick 的前幾個步驟以前。
咱們只是在原生的事件循環之上實現了一個簡單但可用的定時系統。理論上會運行得很好,可是定時是一個很微妙的東西,一個微小的錯誤可能會致使一些很是奇怪的 bug。
在一個複雜的系統之中,設置一些關於定時的規則,而且在以後遵照它們是很是重要的。Nx 制定了以下規則。
setTimeout(fn, 0)
對數據和 DOM 操做的響應,應該按照操做的發生順序來執行。只要他們的執行順序沒有混淆,延遲它們的執行是可取的。把執行順序搞混淆會讓事情變得不可預知和難以找出緣由。
setTimeout(fn, 0)
是徹底不可預知的。用不一樣的方法註冊微任務也會致使混淆執行順序。例如,在下面的示例中, microtask2
將會錯誤地在 microtask1
以前執行。
Promise.resolve().then().then(microtask1)
Promise.resolve().then(microtask2)
複製代碼
把開發者代碼執行時間窗口和內部的操做分開是很重要的。把這兩個混淆將會引發看起來不可預知的行爲,而且最終它將迫使開發者來學習框架的內部工做機制。我認爲許多的前端開發者已經有相似的經歷。
一次事件循環的遍歷叫作 tick,其中執行的代碼稱爲任務。一個 tick 之中只能有一個微任務隊列,一個 tick 之中能夠有多個任務隊列。
同一個任務源的產生的事件必須在同一個任務隊列裏面,而且在每一個任務隊列裏面全部的任務必須按插入的順序執行。微任務隊列中微任務也是會按插入的順序執行。
微任務是在一個 tick 中當前任務結束執行以後開始執行。