淺談React Scheduler任務管理
背景
大家應該都知道,React16採用了fiber架構,這樣的架構下,React內部會動態靈活的管理所有元件的渲染任務,可以隨時暫停某一個元件的渲染,所以,對於複雜型應用來說,對於某一個互動動作的反饋型任務,我們是可以對其進行拆解,一步步的做互動反饋,避免在一個頁面重繪時間週期內做過多的事情,這樣就能減少應用的長任務,最大化提升應用操作效能,那麼,我們現在可以只聚焦在任務管理上,一起來研究一下React到底是如何管理渲染任務的。
閱讀原始碼
很慶幸,React現在的架構已經非常清晰,已經不再是React15那個臃腫,晦澀難懂的時代,React16已經將內部很多模組物理剝離出來,同時目錄結構也越來越清晰,閱讀React原始碼已經是件很輕鬆的事情了。
原始碼地址:
ofollow,noindex"> https:// github.com/facebook/rea ct/blob/master/packages/scheduler/src/Scheduler.js
我們大概的掃一下原始碼,可以看到幾個核心的點:
- 時間
- 優先順序
- requestAnimationFrame
- requestIdleCallback
想要展開去講的話,就必須得先理解頁面的繪製過程,從幀管理到EventLoop中的時間片管理出發,一步步的來理解Facebook這幫人到底是想幹什麼。
瞭解JS執行流程
Javascript執行是會經歷靜態編譯,動態解釋和事件迴圈做任務排程的過程,大致的流程如下(注意,該流程是以chrome瀏覽器核心為標準的執行流程,在node或者其他瀏覽器中,執行流程會有所差異,但是核心思想是差不多的):

該流程的核心特徵是:
- 一個主執行緒負責解析編譯與事件迴圈排程
- 非同步任務佇列與V8通訊是通過Polling Check來實現的
- 非同步任務佇列分為macrotask系統級任務與microtask微任務,它們是按照標準的事件源來分類的
瞭解頁面繪製過程

眾所周知,瀏覽器渲染頁面的流程大概是:
執行JS(具體流程在上面有描述)--->計算Style--->構建佈局模型(Layout)--->繪製圖層樣式(Paint)--->組合計算渲染呈現結果(Composite)
該流程的特徵是:
- 整個過程稱之為一幀
- 幀的渲染過程是在JS執行流程之後或者說一個事件迴圈之後
- 幀的渲染過程是在一個獨立的UI執行緒中處理的,還有GPU執行緒,用於繪製3D檢視
- 幀的渲染與幀的更新呈現是非同步的過程,因為螢幕重新整理頻率是一個固定的重新整理頻率,通常是60次/秒,就是說,渲染一幀的時間要儘可能的低於16.6毫秒,否則在一些高頻次互動動作中是會出現丟幀卡頓的情況,這就是因為渲染幀和重新整理頻率不同步造成的
- 對於離散型互動動作,不要求一幀的渲染時間低於16.6毫秒,但是也是有標準RAIL模型需要遵循的
Google的RAIL模型:
- 以使用者為中心;最終目標不是讓您的網站在任何特定裝置上都能執行很快,而是使使用者滿意。
- 立即響應使用者;在 100ms以內確認使用者輸入,用於離散型互動操作,需要100ms內得以響應
- 設定動畫或滾動時,在 16ms以內生成幀.
- 最大程度增加主執行緒的空閒時間,保證JS單任務執行時間不超過50ms。
- 持續吸引使用者;在 1000ms以內呈現互動內容,首屏秒開。
瞭解requestAnimationFrame
只需要記住幾點:
- 系統控制回撥的執行時機恰好在回撥註冊完成後的下一幀渲染週期的起點的開始執行,控制js計算的到螢幕響應的精確性,避免步調不一致而導致丟幀
- requestAnimationFrame回撥只會在當前頁面啟用狀態下執行,可以大大節省CPU開銷
- 需要注意一點,如果同時在一個高頻次互動過程中註冊多個requestAnimationFrame回撥,這些回撥的執行時機都會被註冊至下一幀渲染週期的起點上,這樣會導致每一幀的渲染壓力增加
- requestAnimationFrame回撥引數是回撥被呼叫的時間,也就是當前幀的起始時間
瞭解requestIdleCallback
var handle = window.requestIdleCallback(callback[, options]);
同樣只需要記住幾點:
- 對於離散型互動,上一幀的渲染到下一幀的渲染時間是屬於系統空閒時間,經過親測,Input輸入,最快的單字元輸入時間平均是33ms(通過持續按同一個鍵來觸發),相當於,上一幀到下一幀中間會存在大於16.4ms的空閒時間,就是說任何離散型互動,最小的系統空閒時間也有16.4ms,也就是說,離散型互動的最短幀長一般是33ms。
- requestIdleCallback回撥呼叫時機是在回撥註冊完成的上一幀渲染到下一幀渲染之間的空閒時間執行
- callback 是要執行的回撥函式,會傳入 deadline 物件作為引數,deadline 包含:
- timeRemaining:剩餘時間,單位 ms,指的是該幀剩餘時間。
- didTimeout:布林型,true 表示該幀裡面沒有執行回撥,超時了。
- options 裡面有個重要引數 timeout,如果給定 timeout,那到了時間,不管有沒有剩餘時間,都會立刻執行回撥 callback。
放棄requestIdleCallback
很遺憾,React16至今都沒有用上requestIdleCallback,從Facebook官方的解釋上來看,主要還是因為瀏覽器的相容性問題,同時React團隊也沒有看到任何瀏覽器廠商在正向的推動requestIdleCallback的覆蓋程序,所以React只能採用了偏hack的polyfill方案。
requestIdleCallback Polyfill方案
很簡單,33毫秒,直接固定死每幀的總時間為33ms,藉助requestAnimationFrame讓一批扁平的任務恰好控制在一塊一塊的33ms這樣的時間片內執行即可
scheduler排程演算法
首先,要明確幾點:
- scheduler是用來做任務排程的
- 所有任務在一個排程生命週期內都有一個過期時間與排程優先順序,但是排程優先順序最終還是會轉換為過期時間,只是過期時間長短的問題,過期時間越短代表越飢餓,優先順序也就越高,但已經過期了的任務也會被視為飢餓任務
- requestAnimationFrameWithTimeout,這是React scheduler的一個超強的函式,它是解決網頁選項卡如果在未啟用狀態下requestAnimationFrame不會被觸發的問題,這樣的話,排程器是可以在後臺繼續做排程的,一方面也能提升使用者體驗,同時後臺執行的時間間隔是以100ms為步長,這個是一個最佳實踐,100ms是不會影響使用者體驗同時也不影響CPU能耗的一個折中時間間隔
- 排程優先順序分為:
- 立即執行優先順序,立即過期
- 使用者阻塞型優先順序,250毫秒後過期
- 空閒優先順序,永不過期,可以在任意空閒時間內執行
- 普通優先順序,5秒後過期
- 一個排程生命週期分為幾個階段
- 排程前
- 註冊任務佇列(環狀連結串列,頭接尾,尾接頭),按照過期時間從小到大排列,如果當前任務是最飢餓的任務,則排到最前面,並立即開始排程,如果並不是最飢餓的任務,則放到佇列中間或者最後面,不做任何操作,等待被排程
- 排程準備
- 通過requestAnimationFrame在下一次螢幕剛開始重新整理的幀起點時計算當前幀的截止時間(33毫秒內)
- 如果不超過當前幀的截止時間且當前任務沒有過期,進入任務排程
- 如果已經超過當前幀的截止時間,但沒有過期,進入下一幀,並更新計算幀截止時間,重新判斷時間(輪詢判斷),直到沒有任何過期超時或者超時才進入任務排程
- 如果已經超過當前幀的截止時間,同時已經過期,進入過期排程
- 正式排程
- 執行排程
- 在當前幀的截止時間前批量呼叫所有任務,不管是否過期
- 過期排程
- 批量呼叫飢餓任務或超時任務的回撥,刪除任務節點
- 排程完成
- 檢查任務佇列是否還有任務
- 先執行最飢餓的任務
- 如果存在任務,則進入下一幀,進入下一個排程生命週期
scheduler的應用場景
最大的應用場景當然是React Fiber,Fiber將每次React渲染打扁成了一系列有序的commit任務,然後通過scheduler來控制每一幀的渲染併發量,從而提升整體效能,就這樣將React渲染過程變成了

這是過去的渲染過程

總結
還記得那年,整天泡在圖書館的日子,為的只是抓住時光流逝的細微瞬間去學習JS技術。
還記得那年,心裡空虛的整日擼著微博,為的只是諮詢各種大V各式各樣的問題。
時間一點點流逝,初心不變,持續學習,持續進步。。。
歡迎來天貓,與我一起探索更有挑戰的前端技術方向!