虛擬DOM

真是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:

cf2089ad62af94881757c2f2de277890.png

  • 創(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)象;
WechatIMG534.png

瀏覽器的引擎工作流程都差不多,如上圖大致分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é)點作比較,不會跨層級比較

2791851910-5becd605889ec_articlex.png

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

d712a73769688afe1ef1a055391d99ed_r.jpg

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í)行刪除操作。

7541670c089b84c59b84e9438e92a8e9_r.jpg

按照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)任務拆分、中斷與恢復

WechatIMG537.png

每一個 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/

Fiber 介紹: http://www.ayqy.net/blog/dive-into-react-fiber/

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容