真是DOM 的缺陷:
- js 操縱Dom 會 影響到整個渲染流水線
- 我們可以調(diào)用document.body.appendChild(node)往 body 節(jié)點上添加一個元素,調(diào)用該 API 之后會引發(fā)一系列的連鎖反應。
1、首先渲染引擎會將 node 節(jié)點添加到 body 節(jié)點之上,
2、然后觸發(fā)樣式計算、布局、繪制、柵格化、合成等任務,我們把這一過程稱為重排。
3、除了重排之外,還有可能引起重繪或者合成操作,形象地理解就是“牽一發(fā)而動全身”。
虛擬dom
虛擬dom需要解決的事情:
1、將頁面改變的內(nèi)容應用到虛擬 DOM 上,而不是直接應用到 DOM 上
2、變化被應用到虛擬 DOM 上時,虛擬 DOM 并不急著去渲染頁面,而僅僅是調(diào)整虛擬 DOM 的內(nèi)部狀態(tài),這樣操作虛擬 DOM 的代價就變得非常輕了
3、在虛擬 DOM 收集到足夠的改變時,再把這些變化一次性應用到真實的 DOM 上
Virtual Dom 的優(yōu)勢在哪里?
一般面試官都會問到Virtual Dom 的優(yōu)勢
面試官不是想聽到
[直接操作/頻繁操作 DOM 的性能差」,如果 DOM 操作的性能如此不堪,那么 jQuery 也不至于活到今天。所以面試官更想聽到 VDOM 想解決的問題以及為什么頻繁的 DOM 操作會性能差。
DOM 引擎、JS 引擎 相互獨立,但又工作在同一線程(主線程)
JS 代碼調(diào)用 DOM API 必須 掛起 JS 引擎、轉換傳入?yún)?shù)數(shù)據(jù)、激活 DOM 引擎,DOM 重繪后再轉換可能有的返回值,最后激活 JS 引擎并繼續(xù)執(zhí)行若有頻繁的 DOM API 調(diào)用,且瀏覽器廠商不做“批量處理”優(yōu)化,
引擎間切換的單位代價將迅速積累若其中有強制重繪的 DOM API 調(diào)用,重新計算布局、重新繪制圖像會引起更大的性能消耗。
- 虛擬 DOM 不會立馬進行排版與重繪操作
- 虛擬 DOM 進行頻繁修改,然后一次性比較并修改真實 DOM 中需要改的部分,最后在真實 DOM 中進行排版與重繪,減少過多DOM節(jié)點排版與重繪損耗
- 虛擬 DOM 有效降低大面積真實 DOM 的重繪與排版,因為最終與真實 DOM 比較差異,可以只渲染局部
除了上面說的帶來的優(yōu)勢,但并不是全部。
虛擬 DOM 最大的優(yōu)勢在于抽象了原本的渲染過程,實現(xiàn)了跨平臺的能力,而不僅僅局限于瀏覽器的 DOM,可以是安卓和 IOS 的原生組件,可以是近期很火熱的小程序,也可以是各種 GUI
直接操作 DOM 的性能并不會低于虛擬 DOM 和 Diff 算法,甚至還會優(yōu)于。
原因是因為:
比較是為了找出不同從而有的放矢的更新頁面。但是比較也是要消耗性能的。而直接操作 DOM 就是有的放矢,我們知道該更新什么不該更新什么,所以不需要有比較的過程。所以直接操作 DOM 效率可能更高
React 厲害的地方并不是說它比 DOM 快,而是說不管你數(shù)據(jù)怎么變化,
我都可以以最小的代價來進行更新 DOM。 方法就是我在內(nèi)存里面用新的數(shù)據(jù)刷新一個虛擬 DOM 樹,然后新舊 DOM 進行比較,找出差異,再更新到 DOM 樹上
React 的出現(xiàn),將命令式變成了聲明式,摒棄了直接操作 DOM 的細節(jié),只關注數(shù)據(jù)的變動,
DOM 操作由框架來完成,從而大幅度提升了代碼的可讀性和可維護性。
框架的意義在于為你掩蓋底層的 DOM 操作
虛擬DOM的作用:
1、Virtual DOM 在犧牲(犧牲很關鍵)部分性能的前提下,增加了可維護性,這也是很多框架的通性。
2、實現(xiàn)了對 DOM 的集中化操作,在數(shù)據(jù)改變時先對虛擬 DOM 進行修改,再反映到真實的 DOM 中,用最小的代價來更新 DOM
3、打開了函數(shù)式 UI 編程的大門
4、可以渲染到 DOM 以外的端,使得框架跨平臺,比如 ReactNative,React VR 等。
5、組件的高度抽象化。
Vue 2.0 引入 vdom 的主要原因是 vdom 把渲染過程抽象化了,從而使得組件的抽象能力也得到提升,并且可以適配 DOM 以外的渲染目標。來自尤大文章:Vue 的理念問題[6]
虛擬DOM 的缺點:
1、首次渲染大量 DOM 時,由于多了一層虛擬 DOM 的計算,會比 innerHTML 插入慢
2、虛擬 DOM 需要在內(nèi)存中的維護一份 DOM 的副本(更上面一條其實也差不多,上面一條是從速度上,這條是空間上)
3、如果虛擬 DOM 大量更改,這是合適的。但是單一的,頻繁的更新的話,虛擬 DOM 將會花費更多的時間處理計算的工作。所以,如果你有一個 DOM 節(jié)點相對較少頁面,用虛擬 DOM,它實際上有可能會更慢。但對于大多數(shù)單頁面應用,這應該都會更快。
react結合虛擬DOM:

創(chuàng)建階段:首先依據(jù) JSX 和基礎數(shù)據(jù)創(chuàng)建出來虛擬 DOM,它反映了真實的 DOM 樹的結構。然后由虛擬 DOM 樹創(chuàng)建出真實 DOM 樹,真實的 DOM 樹生成完后,再觸發(fā)渲染流水線往屏幕輸出頁面
更新階段:如果數(shù)據(jù)發(fā)生了改變,那么就需要根據(jù)新的數(shù)據(jù)創(chuàng)建一個新的虛擬 DOM 樹;然后 React 比較兩個樹,找出變化的地方,并把變化的地方一次性更新到真實的 DOM 樹上;最后渲染引擎更新渲染流水線,并生成新的頁面。
為什么要使用虛擬DOM?
- 前面 說過每次更新一次dom 都會走一次渲染流程,對瀏覽器性能 有很大的影響,可能會造成瀏覽器卡頓的現(xiàn)象;

瀏覽器的引擎工作流程都差不多,如上圖大致分5步:創(chuàng)建DOM tree –> 創(chuàng)建Style Rules -> 構建Render tree -> 布局Layout –> 繪制Painting
1、如果用傳統(tǒng)的 api 或者jq 操作 DOM 樹,瀏覽器會從構建DOM樹開始從頭到尾執(zhí)行一遍流程。
比如:當你一次操作需要更新 10個dom 節(jié)點理想的狀態(tài)是一次性更新完成,但是瀏覽器接受第一個更新的請求之后,不知道后面還有,就會馬上執(zhí)行流程。
緊接著下一次更新 又要重新來一遍,前面的計算也是無用的。
頻繁操作還是會出現(xiàn)頁面卡頓,影響用戶的體驗。
虛擬dom 會把10次更新的diff 保存到內(nèi)存的js 對象中,最后一次性 attach 到dom 樹上。2、具有更強的表達能力,如生命周期和更新時機。
3、執(zhí)行效率高,便于diff
React Dom diff:
傳統(tǒng)diff:
- 在傳統(tǒng)的diff算法下,對比前后兩個節(jié)點,如果發(fā)現(xiàn)節(jié)點改變了,會繼續(xù)去比較節(jié)點的子節(jié)點,一層一層去對比。就這樣循環(huán)遞歸去進行對比,復雜度就達到了o(n3),n是樹的節(jié)點數(shù)
傳統(tǒng)的diff 通過對 循環(huán)遞歸 對節(jié)點對比,效率比較低。算法復雜度達到 O(n^3)。如果 React 只是單純的引入 diff 算法而沒有任何的優(yōu)化改進,那么其效率是遠遠無法滿足前端渲染所要求的性能,那么react 是怎么做的呢?
react diff:
React 通過制定大膽的策略,將 O(n^3) 復雜度的問題轉換成 O(n) 復雜度的問題。
react diff 策略:
策略一(tree diff):Web UI 中 DOM 節(jié)點跨層級的移動操作特別少,可以忽略不計 (DOM結構發(fā)生改變-----直接卸載并重新creat)。
策略二(component diff):擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結構。
策略三(element diff):對于同一層級的一組子節(jié)點,它們可以通過唯一 id 進行區(qū)分。
React 分別對 tree diff、component diff 以及 element diff 進行算法優(yōu)化
- 1、tree diff:
對樹進行分層比較,兩棵樹只會對同一層次的節(jié)點進行比較。
React 只會對同一層的節(jié)點作比較,不會跨層級比較

如果出現(xiàn)了 DOM 節(jié)點跨層級的移動操作,React diff 會有怎樣的表現(xiàn)呢?

A 節(jié)點(包括其子節(jié)點)整個被移動到 D 節(jié)點下,由于 React 只會簡單的考慮同層級節(jié)點的位置變換,而對于不同層級的節(jié)點,只有創(chuàng)建和刪除操作。
當根節(jié)點發(fā)現(xiàn)子節(jié)點中 A 消失了,就會直接銷毀 A;當 D 發(fā)現(xiàn)多了一個子節(jié)點 A,則會創(chuàng)建新的 A(包括子節(jié)點)作為其子節(jié)點。此時,React diff 的執(zhí)行情況:create A -> create B -> create C -> delete A
注意:在開發(fā)組件時,保持穩(wěn)定的 DOM 結構會有助于性能的提升。例如,可以通過 CSS 隱藏或顯示節(jié)點,而不是真的移除或添加 DOM 節(jié)點
2、component diff:
如果是同類型,按照原來策略 繼續(xù)比較 virtual DOM tree;
如果不是一個類型,替換整個組件下的所有子節(jié)點
對于同一類型的組件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切的知道這點那可以節(jié)省大量的 diff 運算時間,因此 React 允許用戶通過 shouldComponentUpdate() 來判斷該組件是否需要進行 diff。
3、element diff:
React diff 提供了三種節(jié)點操作,分別為:INSERT_MARKUP(插入)、MOVE_EXISTING(移動)和 REMOVE_NODE(刪除)
全新的節(jié)點,老的集合里面沒有 ,就重新創(chuàng)建 插入;
可以復用之前的,做了移動;
老 component 類型,在新集合里也有,但對應的 element 不同則不能直接復用和更新,需要執(zhí)行刪除操作,或者老 component 不在新集合里的,也需要執(zhí)行刪除操作。

按照diff 規(guī)則,B != A,則創(chuàng)建并插入 B 至新集合,刪除老集合 A;以此類推,創(chuàng)建并插入 A、D 和 C,刪除 B、C 和 D。這樣比較繁瑣,因為都是相同的節(jié)點,移動就可以減少操作,所以提出了優(yōu)化的方法:
允許開發(fā)者對同一層級的同組子節(jié)點,添加唯一 key 進行區(qū)分,雖然只是小小的改動,性能上卻發(fā)生了翻天覆地的變化
主要分析新老集合中存在相同節(jié)點但位置不同時,對節(jié)點進行位置移動的
總結
- React 通過制定大膽的 diff 策略,將 O(n3) 復雜度的問題轉換成 O(n) 復雜度的問題
- React 通過分層求異的策略,對 tree diff 進行算法優(yōu)化
- React 通過相同類生成相似樹形結構,不同類生成不同樹形結構的策略,對 component diff 進行算法優(yōu)化
- React 通過設置唯一 key的策略,對 element diff 進行算法優(yōu)化
- 建議,在開發(fā)組件時,保持穩(wěn)定的 DOM 結構會有助于性能的提升
我們都知道 react 的核心的思想:
內(nèi)存維護虛擬dom (js 對象),數(shù)據(jù)變化時(setState),自動更新虛擬 DOM,得到一顆新樹,然后 Diff 新老虛擬 DOM 樹,找到有變化的部分,得到一個 Change(Patch),將這個 Patch 加入隊列,最終批量更新這些 Patch 到 DOM 中
調(diào)和階段(Reconciler)
React 會自頂向下通過遞歸,遍歷新數(shù)據(jù)生成新的 Virtual DOM,然后通過 Diff 算法,找到需要變更的元素(Patch),放到更新隊列里面去。
在協(xié)調(diào)階段,采用遞歸的遍歷方法 ,稱為
Stack Reconciler這種方式:
一旦任務開始進行,就無法中斷,那么 js 將一直占用主線程, 一直要等到整棵 Virtual DOM 樹計算完成之后,才能把執(zhí)行權交給渲染引擎,那么這就會導致一些用戶交互、動畫等任務無法立即得到處理,就會有卡頓,非常的影響用戶體驗
針對任務一旦執(zhí)行,無法中斷,js 一直占用主線程導致卡頓的問題如何優(yōu)化?
為什么會出現(xiàn)卡頓?
- 1、處理用戶交互;
- 2、js 解析執(zhí)行
- 3、幀開始。窗口尺寸變更,頁面滾去等的處理
- 4、requestAnimationFrame
- 5、布局
- 6、繪制
以上任意一個環(huán)節(jié)占用的時間過長都用可能造成卡頓的現(xiàn)象;在協(xié)調(diào)階段 js 執(zhí)行過長,那么就有可能本來應該渲染下一幀,但是當前js 還在執(zhí)行,導致卡頓感。
解決方法:
把渲染更新過程拆分成多個子任務,每次只做一小部分,做完看是否還有剩余時間,如果有繼續(xù)下一個任務;如果沒有,掛起當前任務,將時間控制權交給主線程,等主線程不忙的時候在繼續(xù)執(zhí)行。 操作系統(tǒng)常用任務調(diào)度策略之一。
操作系統(tǒng)常用任務調(diào)度策略:
先來先服務(FCFS)調(diào)度算法;
短作業(yè)(進程)優(yōu)先調(diào)度算法(SJ/PF);
最高優(yōu)先權優(yōu)先調(diào)度算法(FPF);
高響應比優(yōu)先調(diào)度算法(HRN);
時間片輪轉法(RR);
多級隊列反饋法。
- 合作式調(diào)度主要就是用來分配任務的,當有更新任務來的時候,不會立馬diff,而是把更新推入 Update Queue 中,然后交給 Scheduler 去處理。
- 兩個執(zhí)行幀之間,主線程通常會有一小段空閑時間,requestIdleCallback可以在這個空閑期(Idle Period)調(diào)用空閑期回調(diào)(Idle Callback),執(zhí)行一些任務。
- requestIdleCallback方法提供 deadline,即任務執(zhí)行限制時間,以切分任務,避免長時間執(zhí)行,阻塞UI渲染而導致掉幀;
1、如何拆分子任務?
2、有剩余時間怎么去調(diào)度應該執(zhí)行哪一個任務?
3、沒有剩余時間之前的任務怎么辦?
對于上面的問題 ,react 通過 Fiber 來解決
渲染階段 (render)
遍歷更新隊列,通過調(diào)用宿主環(huán)境的API,實際更新渲染對應元素。
Fiber
fiber 代表 一種工作單元,需要重新實現(xiàn)一個堆棧幀的調(diào)度,可以按照自己的調(diào)度算法執(zhí)行他們,另外這些調(diào)度可以自己控制,所以本質(zhì)上Fiber 也可以理解為一個虛擬的堆棧幀。 Fiber 是一種數(shù)據(jù)結構(堆棧幀),也可以說是一種解決可中斷的調(diào)用任務的一種解決方案,它的特性就是時間分片(time slicing)和暫停(supense)。
Fiber 是如何工作的?
- ReactDOM.render() 和 setState 的時候開始創(chuàng)建更新;
- 將創(chuàng)建的更新加入任務隊列,等待調(diào)度
- 在 requestIdleCallback 空閑時執(zhí)行任務
- 從根節(jié)點開始遍歷 Fiber Node,并且構建 WokeInProgress Tree。
- 生成 effectList
- 根據(jù) EffectList 更新 DOM。
第一:
從 ReactDOM.render() 方法開始,把接收的 React Element 轉換為 Fiber 節(jié)點,并為其設置優(yōu)先級,創(chuàng)建 Update,加入到更新隊列,這部分主要是做一些初始數(shù)據(jù)的準備
創(chuàng)建ReactRoot 實例 root 調(diào)用root.render -> updateContainer-> 設置優(yōu)先級
第二:
主要是三個函數(shù):scheduleWork、requestWork、performWork,即安排工作、申請工作、正式工作三部曲,React 16 新增的異步調(diào)用的功能則在這部分實現(xiàn),這部分就是 Schedule 階段
第三:
遍歷所有的 Fiber 節(jié)點,通過 Diff 算法計算所有更新工作,產(chǎn)出 EffectList 給到 commit 階段使用,這部分的核心是 beginWork 函數(shù)
Fiber的關鍵特性如下::
- 增量渲染(把渲染任務拆分成塊,勻到多幀)
- 更新時能夠暫停,終止,復用渲染任務
- 給不同類型的更新賦予優(yōu)先級
- 并發(fā)方面新的基礎能力
Fiber Node
{
...
// 跟當前Fiber相關本地狀態(tài)(比如瀏覽器環(huán)境就是DOM節(jié)點)
stateNode: any,
// 單鏈表樹結構
return: Fiber | null,// 指向他在Fiber節(jié)點樹中的`parent`,用來在處理完這個節(jié)點之后向上返回
child: Fiber | null,// 指向自己的第一個子節(jié)點
sibling: Fiber | null, // 指向自己的兄弟結構,兄弟節(jié)點的return指向同一個父節(jié)點
// 更新相關
pendingProps: any, // 新的變動帶來的新的props
memoizedProps: any, // 上一次渲染完成之后的props
updateQueue: UpdateQueue<any> | null, // 該Fiber對應的組件產(chǎn)生的Update會存放在這個隊列里面
memoizedState: any, // 上一次渲染的時候的state
// Scheduler 相關
expirationTime: ExpirationTime, // 代表任務在未來的哪個時間點應該被完成,不包括他的子樹產(chǎn)生的任務
// 快速確定子樹中是否有不在等待的變化
childExpirationTime: ExpirationTime,
// 在Fiber樹更新的過程中,每個Fiber都會有一個跟其對應的Fiber
// 我們稱他為`current <==> workInProgress`
// 在渲染完成之后他們會交換位置
alternate: Fiber | null,
// Effect 相關的
effectTag: SideEffectTag, // 用來記錄Side Effect
nextEffect: Fiber | null, // 單鏈表用來快速查找下一個side effect
firstEffect: Fiber | null, // 子樹中第一個side effect
lastEffect: Fiber | null, // 子樹中最后一個side effect
....
};
Fiber Reconciler
Fiber Reconciler 是 React 里的調(diào)和器,這也是任務調(diào)度完成之后,如何去執(zhí)行每個任務,如何去更新每一個節(jié)點的過程
reconcile 過程分為2個階段(phase):
1、(可中斷)render/reconciliation 通過構造 WorkInProgress Tree 得出 Change。
2、(不可中斷)commit 應用這些DOM change。
reconciliation 階段
由于 reconciliation 階段是可中斷的,一旦中斷之后恢復的時候又會重新執(zhí)行,所以很可能 reconciliation 階段的生命周期方法會被多次調(diào)用
commit 階段
commit 階段可以理解為就是將 Diff 的結果反映到真實 DOM 的過程
commit 階段會執(zhí)行如下的聲明周期方法:
- getSnapshotBeforeUpdate
- componentDidMount
- componentDidUpdate
- componentWillUnmount
注意區(qū)別 reconciler、reconcile 和 reconciliation,reconciler 是調(diào)和器,是一個名詞,可以說是 React 工作的一個模塊,協(xié)調(diào)模塊;reconcile 是調(diào)和器調(diào)和的動作,是一個動詞;而 reconciliation 只是 reconcile 過程的第一個階段。
Fiber Tree 和 WorkInProgress Tree
React 在 render 第一次渲染時,會通過 React.createElement 創(chuàng)建一顆 Element 樹,可以稱之為 Virtual DOM Tree,
由于要記錄上下文信息,加入了 Fiber,每一個 Element 會對應一個 Fiber Node,將 Fiber Node 鏈接起來的結構成為 Fiber Tree。
它反映了用于渲染 UI 的應用程序的狀態(tài)。這棵樹通常被稱為 current 樹(當前樹,記錄當前頁面的狀態(tài))
在后續(xù)的更新過程中(setState),每次重新渲染都會重新創(chuàng)建 Element, 但是 Fiber 不會,F(xiàn)iber 只會使用對應的 Element 中的數(shù)據(jù)來更新自己必要的屬性
Fiber Tree 一個重要的特點是鏈表結構,將遞歸遍歷編程循環(huán)遍歷,然后配合 requestIdleCallback API, 實現(xiàn)任務拆分、中斷與恢復

每一個 Fiber Node 節(jié)點與 Virtual Dom 一一對應,所有 Fiber Node 連接起來形成 Fiber tree, 是個單鏈表樹結構
當調(diào)用 setState 的時候又是如何 Diff 得到 change 的呢
采用的是一種叫雙緩沖技術(double buffering),這個時候就需要另外一顆樹:WorkInProgress Tree,它反映了要刷新到屏幕的未來狀態(tài)
WorkInProgress Tree 構造完畢,得到的就是新的 Fiber Tree,然后喜新厭舊(把 current 指針指向WorkInProgress Tree,丟掉舊的 Fiber Tree)就好了
創(chuàng)建 WorkInProgress Tree 的過程也是一個 Diff 的過程,Diff 完成之后會生成一個 Effect List,這個 Effect List 就是最終 Commit 階段用來處理副作用的階段。
源碼實現(xiàn)(中文版): https://libin1991.github.io/2019/10/25/React-Fiber%E8%B0%83%E5%BA%A6%E5%8E%9F%E7%90%86
源碼實現(xiàn)(英文版): https://pomb.us/build-your-own-react/