前端工程師的自我修養:React Fiber 是如何實現更新過程可控的

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從 React 16 開始,React 採用了 Fiber 機制替代了原先基於原生執行棧遞歸遍歷 VDOM 的方案,提高了頁面渲染性能和用戶體驗。乍一聽 Fiber 好像挺神祕,在原生執行棧都還沒搞懂的情況下,又整出個 Fiber,還能不能愉快的寫代碼了。別慌,老鐵!下面就來嘮嘮關於 Fiber 那點事兒。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"什麼是 Fiber"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Fiber 的英文含義是“纖維”,它是比線程(Thread)更細的線,比線程(Thread)控制得更精密的執行模型。在廣義計算機科學概念中,Fiber 又是一種協作的(Cooperative)編程模型,幫助開發者用一種【既模塊化又協作化】的方式來編排代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/27\/27b86c7d2544c73ae7255afdb8d85a3f.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單點說,Fiber 就是 React 16 實現的一套新的更新機制,讓 React 的更新過程變得可控,避免了之前一竿子遞歸到底影響性能的做法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"關於 Fiber 你需要知道的基礎知識"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. 瀏覽器刷新率(幀)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頁面的內容都是一幀一幀繪製出來的,瀏覽器刷新率代表瀏覽器一秒繪製多少幀。目前瀏覽器大多是 60Hz(60幀\/s),每一幀耗時也就是在 16ms 左右。原則上說 1s 內繪製的幀數也多,畫面表現就也細膩。那麼在這一幀的(16ms) 過程中瀏覽器又幹了啥呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/bc\/bc209547b526114a747dd833840c45a0.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過上面這張圖可以清楚的知道,瀏覽器一幀會經過下面這幾個過程:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"接受輸入事件"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"執行事件回調"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"開始一幀"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"執行 RAF (RequestAnimationFrame)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"頁面佈局,樣式計算"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"渲染"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":7,"align":null,"origin":null},"content":[{"type":"text","text":"執行 RIC  (RequestIdelCallback)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":8,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第七步的 RIC 事件不是每一幀結束都會執行,只有在一幀的 16ms 中做完了前面 6 件事兒且還有剩餘時間,纔會執行。這裏提一下,如果一幀執行結束後還有時間執行 RIC 事件,那麼下一幀需要在事件執行結束才能繼續渲染,所以 RIC 執行不要超過 30ms,如果長時間不將控制權交還給瀏覽器,會影響下一幀的渲染,導致頁面出現卡頓和事件響應不及時。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. JS 原生執行棧"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"React Fiber 出現之前,React 通過原生執行棧遞歸遍歷 VDOM。當瀏覽器引擎第一次遇到 JS 代碼時,會產生一個全局執行上下文並將其壓入執行棧,接下來每遇到一個函數調用,又會往棧中壓入一個新的上下文。比如:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function A(){\n  B();\n  C();\n}\nfunction B(){}\nfunction C(){}\nA();\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"引擎在執行的時候,會形成如下這樣的執行棧:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/00\/009d1d18d319a9a582ac72aa761ce705.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"瀏覽器引擎會從執行棧的頂端開始執行,執行完畢就彈出當前執行上下文,開始執行下一個函數,直到執行棧被清空纔會停止。然後將執行權交還給瀏覽器。由於 React 將頁面視圖視作一個個函數執行的結果。每一個頁面往往由多個視圖組成,這就意味着多個函數的調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果一個頁面足夠複雜,形成的函數調用棧就會很深。每一次更新,執行棧需要一次性執行完成,中途不能幹其他的事兒,只能\"一心一意\"。結合前面提到的瀏覽器刷新率,JS 一直執行,瀏覽器得不到控制權,就不能及時開始下一幀的繪製。如果這個時間超過 16ms,當頁面有動畫效果需求時,動畫因爲瀏覽器不能及時繪製下一幀,這時動畫就會出現卡頓。不僅如此,因爲事件響應代碼是在每一幀開始的時候執行,如果不能及時繪製下一幀,事件響應也會延遲。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3. 時間分片(Time Slicing)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"時間分片指的是一種將多個粒度小的任務放入一個時間切片(一幀)中執行的一種方案,在 React Fiber 中就是將多個任務放在了一個時間片中去執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4. 鏈表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 React Fiber 中用鏈表遍歷的方式替代了 React 16 之前的棧遞歸方案。在 React 16 中使用了大量的鏈表。例如:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用多向鏈表的形式替代了原來的樹結構"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如下面這個組件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"
\n  A1\n  
\n    B1\n     
\n  \n  
\n   B2\n  \n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"會使用下面這樣的鏈表表示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/14\/1409005a85f97c96bc23599c6daa7d41.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"副作用單鏈表"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c9\/c9b0e551e1049a27291fed92eb53f552.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"狀態更新單鏈表"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1d\/1d349304afe5688e671fcd643b79ea6a.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"..."}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鏈表是一種簡單高效的數據結構,它在當前節點中保存着指向下一個節點的指針,就好像火車一樣一節連着一節"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a6\/a6a060613d846f4070157a4cb1c99fed.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遍歷的時候,通過操作指針找到下一個元素。但是操作指針時(調整順序和指向)一定要小心。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鏈表相比順序結構數據格式的好處就是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"操作更高效,比如順序調整、刪除,只需要改變節點的指針指向就好了。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"不僅可以根據當前節點找到下一個節點,在多向鏈表中,還可以找到他的父節點或者兄弟節點。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但鏈表也不是完美的,缺點就是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"比順序結構數據更佔用空間,因爲每個節點對象還保存有指向下一個對象的指針。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"不能自由讀取,必須找到他的上一個節點。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"React 用空間換時間,更高效的操作可以方便根據優先級進行操作。同時可以根據當前節點找到其他節點,在下面提到的掛起和恢復過程中起到了關鍵作用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"React Fiber 是如何實現更新過程可控?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面講完基本知識,現在正式開始介紹今天的主角 Fiber,看看 React Fiber 是如何實現對更新過程的管控。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/3f\/3f3d1fc23788f304fa983a71118133ab.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"更新過程的可控主要體現在下面幾個方面:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"任務拆分"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"任務掛起、恢復、終止"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"任務具備優先級"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. 任務拆分"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面提到,React Fiber 之前是基於原生執行棧,每一次更新操作會一直佔用主線程,直到更新完成。這可能會導致事件響應延遲,動畫卡頓等現象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 React Fiber 機制中,它採用\"化整爲零\"的戰術,將調和階段(Reconciler)遞歸遍歷 VDOM 這個大任務分成若干小任務,每個任務只負責一個節點的處理。例如:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"import React from \"react\";\nimport ReactDom from \"react-dom\"\nconst jsx = (\n    
\n    A1\n    
\n      B1\n      
C1\n      
C2\n    \n    
B2\n  \n)\nReactDom.render(jsx,document.getElementById(\"root\"))\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個組件在渲染的時候會被分成八個小任務,每個任務用來分別處理 A1(div)、A1(text)、B1(div)、B1(text)、C1(div)、C1(text)、C2(div)、C2(text)、B2(div)、B2(text)。再通過時間分片,在一個時間片中執行一個或者多個任務。這裏提一下,所有的小任務並不是一次性被切分完成,而是處理當前任務的時候生成下一個任務,如果沒有下一個任務生成了,就代表本次渲染的 Diff 操作完成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. 掛起、恢復、終止"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再說掛起、恢復、終止之前,不得不提兩棵 Fiber 樹,workInProgress tree 和 currentFiber tree。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"workInProgress 代表當前正在執行更新的 Fiber 樹。在 render 或者 setState 後,會構建一顆 Fiber 樹,也就是 workInProgress tree,這棵樹在構建每一個節點的時候會收集當前節點的副作用,整棵樹構建完成後,會形成一條完整的副作用鏈。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"currentFiber 表示上次渲染構建的 Filber 樹。在每一次更新完成後 workInProgress 會賦值給  currentFiber。在新一輪更新時 workInProgress tree 再重新構建,新 workInProgress 的節點通過 alternate 屬性和 currentFiber 的節點建立聯繫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在新 workInProgress tree 的創建過程中,會同 currentFiber 的對應節點進行 Diff 比較,收集副作用。同時也會複用和 currentFiber 對應的節點對象,減少新創建對象帶來的開銷。也就是說無論是創建還是更新,掛起、恢復以及終止操作都是發生在 workInProgress tree 創建過程中。workInProgress tree  構建過程其實就是循環的執行任務和創建下一個任務,大致過程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/82\/82943272cfb62fefcee943cb845d26e4.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當沒有下一個任務需要執行的時候,workInProgress tree 構建完成,開始進入提交階段,完成真實 DOM 更新。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在構建 workInProgressFiber tree 過程中可以通過掛起、恢復和終止任務,實現對更新過程的管控。下面簡化了一下源碼,大致實現如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"let nextUnitWork = null;\/\/下一個執行單元\n\/\/開始調度\nfunction shceduler(task){\n     nextUnitWork = task; \n}\n\/\/循環執行工作\nfunction workLoop(deadline){\n  let shouldYield = false;\/\/是否要讓出時間片交出控制權\n  while(nextUnitWork && !shouldYield){\n    nextUnitWork = performUnitWork(nextUnitWork)\n    shouldYield = deadline.timeRemaining()<1 \/\/ 沒有時間了,檢出控制權給瀏覽器\n  }\n  if(!nextUnitWork) {\n    conosle.log(\"所有任務完成\")\n    \/\/commitRoot() \/\/提交更新視圖\n  }\n  \/\/ 如果還有任務,但是交出控制權後,請求下次調度\n  requestIdleCallback(workLoop,{timeout:5000}) \n}\n\/*\n * 處理一個小任務,其實就是一個 Fiber 節點,如果還有任務就返回下一個需要處理的任務,沒有就代表整個\n *\/\nfunction performUnitWork(currentFiber){\n  ....\n  return FiberNode\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"掛起"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當第一個小任務完成後,先判斷這一幀是否還有空閒時間,沒有就掛起下一個任務的執行,記住當前掛起的節點,讓出控制權給瀏覽器執行更高優先級的任務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"恢復"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在瀏覽器渲染完一幀後,判斷當前幀是否有剩餘時間,如果有就恢復執行之前掛起的任務。如果沒有任務需要處理,代表調和階段完成,可以開始進入渲染階段。這樣完美的解決了調和過程一直佔用主線程的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼問題來了他是如何判斷一幀是否有空閒時間的呢?答案就是我們前面提到的 RIC (RequestIdleCallback) 瀏覽器原生 API,React 源碼中爲了兼容低版本的瀏覽器,對該方法進行了 Polyfill。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當恢復執行的時候又是如何知道下一個任務是什麼呢?答案在前面提到的鏈表。在 React Fiber 中每個任務其實就是在處理一個 FiberNode 對象,然後又生成下一個任務需要處理的 FiberNode。順便提一嘴,這裏提到的FiberNode 是一種數據格式,下面是它沒有開美顏的樣子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"class FiberNode {\n  constructor(tag, pendingProps, key, mode) {\n    \/\/ 實例屬性\n    this.tag = tag; \/\/ 標記不同組件類型,如函數組件、類組件、文本、原生組件...\n    this.key = key; \/\/ react 元素上的 key 就是 jsx 上寫的那個 key ,也就是最終 ReactElement 上的\n    this.elementType = null; \/\/ createElement的第一個參數,ReactElement 上的 type\n    this.type = null; \/\/ 表示fiber的真實類型 ,elementType 基本一樣,在使用了懶加載之類的功能時可能會不一樣\n    this.stateNode = null; \/\/ 實例對象,比如 class 組件 new 完後就掛載在這個屬性上面,如果是RootFiber,那麼它上面掛的是 FiberRoot,如果是原生節點就是 dom 對象\n    \/\/ fiber\n    this.return = null; \/\/ 父節點,指向上一個 fiber\n    this.child = null; \/\/ 子節點,指向自身下面的第一個 fiber\n    this.sibling = null; \/\/ 兄弟組件, 指向一個兄弟節點\n    this.index = 0; \/\/  一般如果沒有兄弟節點的話是0 當某個父節點下的子節點是數組類型的時候會給每個子節點一個 index,index 和 key 要一起做 diff\n    this.ref = null; \/\/ reactElement 上的 ref 屬性\n    this.pendingProps = pendingProps; \/\/ 新的 props\n    this.memoizedProps = null; \/\/ 舊的 props\n    this.updateQueue = null; \/\/ fiber 上的更新隊列執行一次 setState 就會往這個屬性上掛一個新的更新, 每條更新最終會形成一個鏈表結構,最後做批量更新\n    this.memoizedState = null; \/\/ 對應  memoizedProps,上次渲染的 state,相當於當前的 state,理解成 prev 和 next 的關係\n    this.mode = mode; \/\/ 表示當前組件下的子組件的渲染方式\n    \/\/ effects\n    this.effectTag = NoEffect; \/\/ 表示當前 fiber 要進行何種更新\n    this.nextEffect = null; \/\/ 指向下個需要更新的fiber\n    this.firstEffect = null; \/\/ 指向所有子節點裏,需要更新的 fiber 裏的第一個\n    this.lastEffect = null; \/\/ 指向所有子節點中需要更新的 fiber 的最後一個\n    this.expirationTime = NoWork; \/\/ 過期時間,代表任務在未來的哪個時間點應該被完成\n    this.childExpirationTime = NoWork; \/\/ child 過期時間\n    this.alternate = null; \/\/ current 樹和 workInprogress 樹之間的相互引用\n  }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"額…看着好像有點上頭,這是開了美顏的樣子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ab\/ab8866fb1ac3eb4d9c5737239565da05.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"是不是好看多了?在每次循環的時候,找到下一個執行需要處理的節點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function performUnitWork(currentFiber){\n    \/\/beginWork(currentFiber) \/\/找到兒子,並通過鏈表的方式掛到currentFiber上,每一偶兒子就找後面那個兄弟\n  \/\/有兒子就返回兒子\n  if(currentFiber.child){\n    return currentFiber.child;\n  } \n  \/\/如果沒有兒子,則找弟弟\n  while(currentFiber){\/\/一直往上找\n    \/\/completeUnitWork(currentFiber);\/\/將自己的副作用掛到父節點去\n    if(currentFiber.sibling){\n      return currentFiber.sibling\n    }\n    currentFiber = currentFiber.return;\n  }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一次任務結束後返回該處理節點的子節點或兄弟節點或父節點。只要有節點返回,說明還有下一個任務,下一個任務的處理對象就是返回的節點。通過一個全局變量記住當前任務節點,當瀏覽器再次空閒的時候,通過這個全局變量,找到它的下一個任務需要處理的節點恢復執行。就這樣一直循環下去,直到沒有需要處理的節點返回,代表所有任務執行完成。最後大家手拉手,就形成了一顆 Fiber 樹。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/82\/82a2d59c889af9eeff648374a0bd3e39.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"終止"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實並不是每次更新都會走到提交階段。當在調和過程中觸發了新的更新,在執行下一個任務的時候,判斷是否有優先級更高的執行任務,如果有就終止原來將要執行的任務,開始新的 workInProgressFiber 樹構建過程,開始新的更新流程。這樣可以避免重複更新操作。這也是在 React 16 以後生命週期函數 componentWillMount 有可能會執行多次的原因。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3. 任務具備優先級"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"React Fiber 除了通過掛起,恢復和終止來控制更新外,還給每個任務分配了優先級。具體點就是在創建或者更新 FiberNode 的時候,通過算法給每個任務分配一個到期時間(expirationTime)。在每個任務執行的時候除了判斷剩餘時間,如果當前處理節點已經過期,那麼無論現在是否有空閒時間都必須執行改任務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/02\/0257b9fa0f5f7542ecaea909956f0b83.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時過期時間的大小還代表着任務的優先級。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"任務在執行過程中順便收集了每個 FiberNode 的副作用,將有副作用的節點通過 firstEffect、lastEffect、nextEffect 形成一條副作用單鏈表 AI(TEXT)-B1(TEXT)-C1(TEXT)-C1-C2(TEXT)-C2-B1-B2(TEXT)-B2-A。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/77\/77f0a4230ac0152f2eaf3b23da625b35.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實最終都是爲了收集到這條副作用鏈表,有了它,在接下來的渲染階段就通過遍歷副作用鏈完成 DOM 更新。這裏需要注意,更新真實 DOM 的這個動作是一氣呵成的,不能中斷,不然會造成視覺上的不連貫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"關於 React Fiber 的思考"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. 能否使用生成器(generater)替代鏈表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Fiber 機制中,最重要的一點就是需要實現掛起和恢復,從實現角度來說 generator 也可以實現。那麼爲什麼官方沒有使用 generator 呢?猜測應該是是性能方面的原因。生成器不僅讓您在堆棧的中間讓步,還必須把每個函數包裝在一個生成器中。一方面增加了許多語法方面的開銷,另外還增加了任何現有實現的運行時開銷。性能上遠沒有鏈表的方式好,而且鏈表不需要考慮瀏覽器兼容性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. Vue 是否會採用 Fiber 機制來優化複雜頁面的更新"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個問題其實有點搞事情,如果 Vue 真這麼做了是不是就是變相承認 Vue 是在\"集成\" Angular 和 React 的優點呢?React 有 Fiber,Vue 就一定要有?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"兩者雖然都依賴 DOM Diff,但是實現上卻有區別,DOM Diff 的目的都是收集副作用。Vue 通過 Watcher 實現了依賴收集,本身就是一種很好的優化。所以 Vue 沒有采用 Fiber 機制,也無傷大雅。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"React Fiber 的出現相當於是在更新過程中引進了一箇中場指揮官,負責掌控更新過程,足球世界裏管這叫前腰。拋開帶來的性能和效率提升外,這種“化整爲零”和任務編排的思想,可以應用到我們平時的架構設計中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:赤墨"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:https:\/\/mp.weixin.qq.com\/s\/i6xbbIEZC9p4gNl505PKqg"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:前端工程師的自我修養:React Fiber 是如何實現更新過程可控的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:政採雲前端團隊 - 微信公衆號 [ID:Zoo-Team]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章