React更新渲染原理深入分析
當我們調(diào)用 setState 之后發(fā)生了什么?react經(jīng)歷了怎樣的過程將新的 state 渲染到頁面上?
一次react更新,核心就是對虛擬dom進行diff,找出最少的需要變化的dom節(jié)點,然后對其進行相應的dom操作,用戶即可在頁面上看到更新。但 react 作為廣泛使用的框架,需要考慮更多的因素,考慮多個更新的優(yōu)先級,考慮主線程占用時長,考慮diff算法復雜度,考慮性能。。等等,本文就來探討一下react在其內(nèi)部是如何處理數(shù)據(jù)更新的。
react在內(nèi)部使用fiber這種數(shù)據(jù)結構來作為虛擬dom【react16+】,它與dom tree一一對應,形成fiber tree,一次react更新,本質(zhì)是fiber tree結構的更新變化。而fiber tree結構的更新,用更專業(yè)的術語來講,其實就是fiber tree的協(xié)調(diào)(Reconcile)。Reconcile中文意思是調(diào)和、使一致,協(xié)調(diào)fiber tree,就是調(diào)整fiber tree的結構,使其和更新后的jsx模版結構、dom tree保持一致。
react從16起,將更新機制分為三個模塊,也可以說是三個步驟,分別是Schedule【調(diào)度】、Reconcile【協(xié)調(diào)】、render【渲染】
Schedule
為什么需要Schedule?
首先我們要知道react在進行協(xié)調(diào)時,提供了兩種模式:Legacy mode 同步阻塞模式和 Concurrent mode 并行模式。
不同上下文中的更新會觸發(fā)不同的模式,如果是在 event、setTimeout、network request 的 callback 中觸發(fā)更新,react 會采用 Legacy 模式。如果更新與 Suspense、useTransition、OffScreen 相關,那么 react 會采用 Concurrent 模式。
Legacy mode
Legacy mode在協(xié)調(diào)時會啟動 workLoopSync。workLoopSync 開始工作以后,要等到所有 fiber node 都處理完畢以后,才會結束工作,也就是 fiber tree 的協(xié)調(diào)過程不可中斷。
Legacy mode存在的問題:如果 fiber tree 的結構很復雜,那么協(xié)調(diào) fiber tree 可能會占用大量的時間,導致主線程會一直被 js 引擎占用,渲染引擎無法在規(guī)定時間(瀏覽器刷新頻率 - 16.7ms)內(nèi)完成工作,使得頁面出現(xiàn)卡頓(掉幀),影響用戶體驗。
Concurrent mode
鑒于Legacy mode存在的問題,react團隊在react 16中提出了 Concurrent mode的概念,并在react 18中開放使用。react16、17一直為此做準備。
Concurrent 模式最大的意義在于,使用Concurrent 模式以后的react的應用可以做到:
- 協(xié)調(diào)可以中斷、恢復;不會長時間阻塞瀏覽器渲染
- 高優(yōu)先級更新可以中斷低優(yōu)先級更新,優(yōu)先渲染
那么,怎么做到這兩點呢?
事實上,Schedule就是用來完成這個任務的,調(diào)度任務的優(yōu)先級,使高優(yōu)先級任務優(yōu)先進入Reconcile,并且提供中斷和恢復機制。
時間切片
react采用時間切片的方式來實現(xiàn)協(xié)調(diào)的中斷和恢復,Concurrent mode在協(xié)調(diào)時會啟動 workLoopConcurrent。 workLoopConcurrent 開始工作以后,每次協(xié)調(diào) fiber node 時,都會判斷當前時間片是否到期。如果時間片到期,會停止當前 workLoopConcurrent,讓出主線程,然后請求下一個時間片繼續(xù)協(xié)調(diào)。
協(xié)調(diào)的中斷及恢復,類似于瀏覽器的eventloop,js引擎和渲染引擎互斥,在主線程中交替工作。
我們可以通過模擬 eventLoop來實現(xiàn)時間分片以及重新請求時間片。一段 js 程序,如果在規(guī)定時間內(nèi)沒有結束,那我們可以主動結束它,然后請求一個新的時間片,在下一個時間片內(nèi)繼續(xù)處理上一次沒有結束的任務。
let taskQueue = []; // 任務列表
let shouldTimeEnd = 5ms; // 一個時間片定義為 5ms
let channel = new MessageChannel(); // 創(chuàng)建一個 MessageChannel 實例
function wookLoop() {
let beginTime = performance.now(); // 記錄開始時間
while(true) { // 循環(huán)處理 taskQueue 中的任務
let currentTime = performance.now(); // 記錄下一個任務開始時的時間
if (currentTime - beginTime >= shouldTimeEnd) break; // 時間片已經(jīng)到期,結束任務處理
processTask(); // 時間片沒有到期,繼續(xù)處理任務
}
if (taskQueue.length) { // 時間片到期,通過調(diào)用 postMessage,請求下一個時間片
channel.port2.postMessage(null);
}
}
channel.port1.onmessage = wookLoop; // 在下一個時間片內(nèi)繼續(xù)處理任務
workLoop();
和瀏覽器的消息隊列 一樣, react 也會維護一個任務隊列 taskQueue,然后通過 workLoop 遍歷 taskQueue,依次處理 taskQueue 中的任務。
taskQueue 中收集任務是有先后處理順序的,workLoop 每次處理 taskQueue 中的任務時,都會挑選優(yōu)先級最高的任務進行處理。
每觸發(fā)一次 react 更新,意味著一次 fiber tree 的協(xié)調(diào),但協(xié)調(diào)并不會在更新觸發(fā)時立刻同步進行。相反,react 會為這一次更新,生成一個 task,并添加到 taskQueue 中,fiber tree 的協(xié)調(diào)方法會作為新建 task 的 callback。當 wookLoop 開始處理該 task 時,才會觸發(fā) task 的 callback,開始 fiber tree 的協(xié)調(diào)。
任務的優(yōu)先級
react在內(nèi)部定義了 5 種類型的優(yōu)先級,以及對應的超時時間timeout
ImmediatePriority, 直接優(yōu)先級,對應用戶的click、input、focus等操作;timeout為 -1,表示任務要盡快處理;UserBlockingPriority,用戶阻塞優(yōu)先級,對應用戶的mousemove、scroll等操作;timeout為 250 ms;NormalPriority,普通優(yōu)先級,對應網(wǎng)絡請求、useTransition等操作;timeout為 5000 ms;LowPriority,低優(yōu)先級(未找到應用場景);timeout為 10000 ms;IdlePriority,空閑優(yōu)先級,如OffScreen;timeout為 1073741823 ms;
5 種優(yōu)先級的順序為: ImmediatePriority > UserBlockingPriority > NormalPriority > LowPriority > IdlePriority。
在確定了任務的優(yōu)先級以后,react 會根據(jù)優(yōu)先級為任務計算一個過期時間 expirationTime,即 expirationTime = currentTime + timeout,然后根據(jù) expirationTime 時間來決定任務處理的先后順序。
expirationTime越小的任務會被排在task隊列的越前面,之所以需要timeout,而不是直接對比優(yōu)先級等級,是為了避免低優(yōu)先級任務長時間被 插隊而導致一直無響應;同時,在時間分片到期時,需要根據(jù)expirationTime判斷下一個要處理的任務是否過期,如果已過期,就不能讓出主線程,需要立即處理。
??注:react17中用Lanes重構了優(yōu)先級算法,此處不展開陳述,有興趣的同學可查閱相關文檔。
獲取最先處理的task
react 采用了小頂堆來存儲task,實現(xiàn)最小優(yōu)先隊列,即 taskQueue 是一個小頂堆,放在堆頂?shù)?code>task是需要最先處理的。
使用最小堆時,有三個操作:push、pop、peek。
push,入堆操作,即將 task 添加到 taskQueue 中。添加一個新創(chuàng)建的 task 時,會將 task 添加到最小堆的堆底,然后對最小堆做自底向上的調(diào)整。調(diào)整時,會比較堆節(jié)點(task) 的 expirationTime,將 expirationTime 較小的 task 向上調(diào)整。* peek,獲取堆頂元素,即獲取需要最先處理的 task,執(zhí)行 task 的 callback,開始 fiber tree 的協(xié)調(diào)。* pop,堆頂元素出堆,即 task 處理完畢,從 taskQueue 中移除。移除堆頂元素以后,會對最小堆做自頂向下的調(diào)整。調(diào)整時,也是比較堆節(jié)點(task) 的 expirationTime,將 expirationTime 較大的 task 向下調(diào)整。### 高優(yōu)先級的更新中斷低優(yōu)先級的更新
Concurrent 模式下,如果在低優(yōu)先級更新的協(xié)調(diào)過程中,有高優(yōu)先級更新進來,那么高優(yōu)先級更新會中斷低優(yōu)先級更新的協(xié)調(diào)過程。
每次拿到新的時間片以后,workLoopConcurrent 都會判斷本次協(xié)調(diào)對應的優(yōu)先級和上一次時間片到期中斷的協(xié)調(diào)的優(yōu)先級是否一樣。如果一樣,說明沒有更高優(yōu)先級的更新產(chǎn)生,可以繼續(xù)上次未完成的協(xié)調(diào);如果不一樣,說明有更高優(yōu)先級的更新進來,此時要清空之前已開始的協(xié)調(diào)過程,從根節(jié)點開始重新協(xié)調(diào)。等高優(yōu)先級更新處理完成以后,再次從根節(jié)點開始處理低優(yōu)先級更新。
Reconcile
前面說到,reconcile(協(xié)調(diào))就是fiber tree 結構的更新,那么具體是怎樣更新的呢?本小節(jié)就來解答這個問題。
前置知識
從jsx到dom

Step1: 從jsx生成react element:
jsx 模板通過 babel 編譯為 createElement 方法;執(zhí)行組件方法,觸發(fā) createElement 的執(zhí)行,返回 react element;
Step2: 從react element生成fiber tree:
fiber tree中存在三種類型的指針child、sibling、return。其中,child指向第一個子節(jié)點,sibling指向兄弟節(jié)點,return指針指向父節(jié)點;*fiber tree采用的深度優(yōu)先遍歷,如果節(jié)點有子節(jié)點,先遍歷子節(jié)點;子節(jié)點遍歷結束以后,再遍歷兄弟節(jié)點;沒有子節(jié)點、兄弟節(jié)點,就返回父節(jié)點,遍歷父節(jié)點的兄弟節(jié)點;* 當節(jié)點的return指針返回null時,fiber tree的遍歷結束;Step3:fiber tree生成之后,從fiber tree到真實dom,就是處理fiber tree上對應的副作用,包括:- 所有
dom節(jié)點的新增; componentDidMount、useEffect的callback函數(shù)的觸發(fā);ref引用的初始化;
雙緩存fiber tree
react 做更新處理時,會同時存在兩顆 fiber tree。一顆是已經(jīng)存在的 old fiber tree,對應當前屏幕顯示的內(nèi)容,稱為 current fiber tree;另外一顆是更新過程中構建的 new fiber tree,稱為 workInProgress fiber tree。
current fiber tree和workInProgress fiber tree可以通過alternate指針互相訪問
當更新完成以后,使用 workInProgress fiber tree 替換掉 current fiber tree,作為下一次更新的 current fiber tree。
協(xié)調(diào)的過程
協(xié)調(diào)過程中主要做三件事情:
1.為 workInProgress fiber tree 生成 fiber node;
2.為發(fā)生變化的 fiber node標記副作用 effect;
3.收集帶 effect 的 fiber node;
生成workInProgress fiber tree
workInProgress fiber tree 作為一顆新樹,生成 fiber node 的方式有三種:
- 克隆(淺拷貝)
current fiber node,意味著原來的dom節(jié)點可以復用,只需要更新dom節(jié)點的屬性,或者移動dom節(jié)點; - 新建一個
fiber node,意味著需要新增加一個dom節(jié)點; - 直接復用
current fiber node,表示對應的dom節(jié)點完全不用做任何處理;
復用的場景:當子組件的渲染方法(類組件的 render、函數(shù)組件方法)沒有觸發(fā),(比如使用了React.memo),沒有返回新的 react element,子節(jié)點就可以直接復用 current fiber node。
在日常開發(fā)過程中,我們可以通過合理使用 ShouldComponentUpdate、React.memo,阻止不必要的組件重新 render,通過直接復用 current fiber node,加快 workInProgress fiber tree 的協(xié)調(diào),達到優(yōu)化的目的。
相反,只要組件的渲染方法被觸發(fā),返回新的 react element,那么就需要根據(jù)新的 react element 為子節(jié)點創(chuàng)建 fiber node(通過淺拷貝或新建)。
- 如果能在
current fiber tree中找到匹配節(jié)點,那么可以通過克隆(淺拷貝)current fiber node的方式來創(chuàng)建新的節(jié)點; - 相反,如果無法在
current fiber tree找到匹配節(jié)點,那么就需要重新創(chuàng)建一個新的節(jié)點;
我們常說的diff算法就是發(fā)生在這一環(huán)節(jié)。
diff算法比較的雙方是 workInProgress fiber tree 中用于構建 fiber node 的 react element 和 current fiber tree 中的 fiber node,比較兩者的 key 和 type,根據(jù)比較結果來決定如何為 workInProgress fiber tree 創(chuàng)建 fiber node。
【 key 和 type 】:
key就是 jsx 模板中元素上的 key 屬性。如果不寫默認為undefined。jsx 模板轉(zhuǎn)化為 react element 后,元素的 key 屬性會作為 react element 的 key 屬性。同樣的,react element 轉(zhuǎn)化為 fiber node 以后,react element 的 key 屬性也會作為 fiber node 的 key 屬性。
jsx 中不同的元素類型,有不同的type:
<Component name="xxxx" /> //type = Component, 是一個函數(shù) <div></div> // type = "div", 是一個字符串 <React.Fragment></React.Fragment> // type = React.Fragment, 是一個數(shù)字(react 內(nèi)部定義的);
jsx 模板轉(zhuǎn)化為 react element 以后,react element 的 type 屬性會根據(jù) jsx 元素的類型賦不同的值,可能是組件函數(shù),也可能是 dom 標簽字符串,還可能是數(shù)字。 react element 轉(zhuǎn)化為 fiber node 以后,react element 的 type 屬性也會作為 fiber node 的 type 屬性。
綜上,判斷拷貝 current fiber node 的邏輯,概括來就是:
reactElement.key === currentFiberNode.key && reactElement.type === currentFiberNode.type, current fiber node //可以克??; reactElement.key !== currentFiberNode.key, current fiber node //不可克?。? reactElement.key === currentFiberNode.key && reactElement.type !== currentFiberNode.type, current fiber node //不可克?。?
diff 算法:
- 已匹配的父節(jié)點的直接子節(jié)點進行比較,不跨父節(jié)點比較;
- 通過比較
key、type來判斷是否需要克隆current fiber node。只有key和type都相等,才克隆current fiber node作為新的節(jié)點,否則就需要新建一個節(jié)點。key值和節(jié)點類型type,key的優(yōu)先級更高。如果key值不相同,那么節(jié)點不可克隆。 - 當比較
single react element和current fiber node list時,只需要遍歷current fiber node list,比較每個current fiber node和react element的key值和type。只有key和type都相等,react element和current fiber node才能匹配。如果有匹配的,直接克隆current fiber node,作為react element對應的workInProgress fiber node。如果沒有匹配的current fiber node,就需要為react element重新創(chuàng)建一個新的fiber node作為workInProgress fiber node。 - 當比較
react element list和current fiber node list時,還需要通過列表下標index判斷wokrInProgress fiber node是否相對于克隆的current fiber node發(fā)生了移動。這也是diff中最復雜的地方。
為發(fā)生變化的fiber node標記effect
判斷節(jié)點是否發(fā)生變化
- 節(jié)點只要是重新創(chuàng)建的而不是克隆自
current fiber node,那么節(jié)點就百分之百發(fā)生了變化,需要更新;* 節(jié)點克隆自current fiber node,需要比較props是否發(fā)生了變化,如果props發(fā)生了變化,節(jié)點需要更新;* 節(jié)點克隆自current fiber node,且是組件類型,還需要比較state是否發(fā)生了變化,如果state發(fā)生了變化,節(jié)點需要更新;常見的effect類型: Placement,放置,只針對dom類型的fiber node,表示節(jié)點需要做移動或者添加操作。Update,更新,針對所有類型的fiber node,表示fiber node需要做更新操作。PlacementAndUpdate,放置并更新,只針對dom類型的fiber node,表示節(jié)點發(fā)生了移動且props發(fā)生了變化。Ref,表示節(jié)點存在ref,需要初始化 / 更新ref.current。Deletion,刪除,針對所有類型的fiber node,表示fiber node需要移除。Snapshot,快照,主要是針對類組件fiber node。當類組件fiber node發(fā)生了mount或者update操作,且定義了getSnapshotBeforeUpdate方法,就會標記Snapshot。Passive,主要針對函數(shù)組件fiber node,表示函數(shù)組件使用了useEffect。當函數(shù)組件節(jié)點發(fā)生mount或者update操作,且使用了useEffect hook,就會給fiber node標記Passive。Layout,主要針對函數(shù)組件fiber node,表示函數(shù)組件使用了useLayoutEffect。當函數(shù)組件節(jié)點發(fā)生mount或者update操作,且使用了useLayoutEffect hook,就會給fiber node標記Layout。
react 使用二進制數(shù)來聲明 effect,如 Placement 為 2 (0000 0010),Update 為 4 (0000 0100)。一個 fiber node 可同時標記多個 effect,如函數(shù)組件 props 發(fā)生變化且使用了 useEffect hook,那么就可以使用 Placement | Update = 516(位運算符) 來標記。
收集帶effect的fiber node
如果一個 fiber node 被標記了 effect,那么 react 就會在這個 fiber node 完成協(xié)調(diào)以后,將這個 fiber node 收集到effectList中。當整顆 fiber tree 完成協(xié)調(diào)以后,所有被標記 effect 的 fiber node 都被收集到一起。
收集fiber node的 effectList 采用單鏈表結構存儲,firstEffect 指向第一個標記 effect 的 fiber node,lastEffect 標記最后一個 fiber node,節(jié)點之間通過 nextEffect 指針連接。
由于 fiber tree 協(xié)調(diào)時采用的順序是深度優(yōu)先,協(xié)調(diào)完成的順序是子節(jié)點、子節(jié)點兄弟節(jié)點、父節(jié)點,所以收集帶 effect 標記的 fiber node 時,順序也是子節(jié)點、子節(jié)點兄弟節(jié)點、父節(jié)點。
Render
render也稱為commit,是對協(xié)調(diào)過程中標記的effect 的處理
effect 的處理分為三個階段,這三個階段按照從前到后的順序為:
1.before mutation 階段 (dom 操作之前)
2.mutation 階段 (dom 操作)
3.layout 階段 (dom 操作之后)
不同的階段,處理的 effect 種類也不相同。在每個階段,react 都會從 effectList 鏈表的頭部 - firstEffect 開始,按序遍歷 fiber node, 直到 lastEffect。
before mutation階段
before mutation 階段的主要工作是處理帶 Snapshot 標記的 fiber node。 從 firstEffect 開始遍歷 effect 列表,如果 fiber node 帶 Snapshot 標記,觸發(fā) getSnapshotBeforeUpdate 方法。
mutation階段
mutation 階段的主要工作是處理帶 Deletion 、 Placement、PlacementAndUpdate、Update 標記的 fiber node。 在這一階段,涉及到 dom 節(jié)點的更新、新增、移動、刪除,組件節(jié)點刪除導致的 componentWillUnmount、destory 方法的觸發(fā),以及刪除節(jié)點引發(fā)的 ref 引用的重置。
dom 節(jié)點的更新:
- 通過原生的 API
setAttribute、removeArrribute修改dom節(jié)點的attr; - 直接修改
dom節(jié)點的style; - 直接修改
dom節(jié)點的innerHtml、textContent;
dom 節(jié)點的新增和移動:
- 如果新增(移動)的節(jié)點是父節(jié)點的最后一個子節(jié)點,那么可以直接使用
appendChild方法。 - 如果不是最后一個節(jié)點,需要使用
insertBefore方法。通過遍歷找到第一個沒有帶Placement標記的節(jié)點作為insertBefore的定位元素。
dom節(jié)點的刪除:
- 如果節(jié)點是
dom節(jié)點,通過removeChild移除; - 如果節(jié)點是組件節(jié)點,觸發(fā)
componentWillUnmount、useEffect的destory方法的執(zhí)行; - 如果標記
Deletion的節(jié)點的子節(jié)點中有組件節(jié)點,深度優(yōu)先遍歷子節(jié)點,依次觸發(fā)子節(jié)點的componentWillUnmount、useEffect的destory方法的執(zhí)行; - 如果標記
Deletion的節(jié)點及子節(jié)點關聯(lián)了ref引用,要將ref引用置空,及ref.current=null(也是深度優(yōu)先遍歷);
layout 階段
layout 階段的主要工作是處理帶 update 標記的組件節(jié)點和帶 ref 標記的所有節(jié)點。 工作內(nèi)容如下:
- 如果類組件節(jié)點是
mount操作,觸發(fā)componentDidMount;如果是update操作,觸發(fā)componentDidUpdate; - 如果函數(shù)組件節(jié)點時
mount操作,觸發(fā)useLayoutEffect的callback;如果是update操作,先觸發(fā)上一次更新生成的destory,再觸發(fā)這一次的callback; - 異步調(diào)度函數(shù)組件的
useEffect; - 如果組件節(jié)點關聯(lián)了
ref引用,要初始化ref.current;
到此這篇關于React更新渲染原理深入分析的文章就介紹到這了,更多相關React更新渲染內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
React Native 集成jpush-react-native的示例代碼
這篇文章主要介紹了React Native 集成jpush-react-native的示例代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08
解決React報錯Style prop value must be a
這篇文章主要為大家介紹了React報錯Style prop value must be an object解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12
React實現(xiàn)一個支持動態(tài)插槽的Layout組件
這篇文章主要為大家詳細介紹了如何使用React實現(xiàn)一個支持動態(tài)注冊內(nèi)容的插槽組件,文中的示例代碼簡潔易懂,有需要的小伙伴可以了解一下2025-02-02
在React中使用React.createRef:更優(yōu)雅的DOM引用方式
React提供了多種方式來引用DOM元素,其中React.createRef()是一種更為現(xiàn)代、更優(yōu)雅的方式,在這篇文章中,我們將深入了解React.createRef()的應用,以及它為開發(fā)者帶來的便利,感興趣的朋友一起看看吧2024-01-01

