1. 程式人生 > 程式設計 >React Diff原理深入分析

React Diff原理深入分析

在瞭解Diff前,先看下React的虛擬DOM的結構

這是html結構

<div id="father">
  <p class="child">I am child p</p>
  <div class="child">I am child div</div>
</div>

這是React渲染html時的js程式碼 自己可以在babel上試試

React.createElement("div",{id: "father"},React.createElement("p",{class: "child"},"I am child p"),React.createElement("div","I am child div")
);

由此可以看出這是一個樹結構

React Diff原理深入分析

React在呼叫render方法時會建立一顆樹(簡稱pre),在下一次呼叫render方法時會返回一顆不同的樹(簡稱cur)。React就會比較pre和cur這兩棵樹之間的差別來判斷如何高效的更新UI,保證當前UI與最新的樹cur保持同步。

為了高效更新UI,React在以下兩個假設的基礎上提出了一套O(n)的啟發演算法:

1.兩個不同型別的元素會產生出不同的樹;

2.開發者可以通過設定key屬性,來告知渲染哪些子元素在不同的渲染下可以儲存不變;

Diffing 演算法

逐層比較

在對比兩棵樹時,React是逐層進行比較的,只會對相同顏色框內的DOM節點進行比較。

React Diff原理深入分析

首先比較兩棵樹的根節點,不同型別的根節點會有不同的形態。當根節點為不同型別的元素時,React 會拆卸原有的樹並且建立起新的樹。舉個例子,當一個元素從<a>變成<img>,從<Article>變成<Comment>,或從<Button>變成<div>都會觸發一個完整的重建流程。

//before
<div>
    <App/>
</div>
//after
<p>
    <App/>
</p>

React會銷燬App元件(該元件的子元件也全都銷燬),並且重新建立一個新的App元件(也包括App的子元件)。

如下的DOM結構轉換:

React Diff原理深入分析

React只會簡單的考慮同層節點的位置變換,對於不同層的節點,只有簡單的建立和刪除。當根節點發現子節點中A不見了,就會直接銷燬A;而當D發現自己多了一個子節點A,則會建立一個新的A作為子節點。因此對於這種結構的轉變的實際操作是:

A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);

雖然看上去這樣的演算法有些“簡陋”,但是其基於的是第一個假設:兩個不同型別的元素會產生出不同的樹。根據React官方文件,這一假設至今為止沒有導致嚴重的效能問題。這當然也給我們一個提示,在實現自己的元件時,保持穩定的DOM結構會有助於效能的提升。例如,我們有時可以通過css隱藏或顯示某些節點,而不是真的移除或新增DOM節點。

對比同類型的元件元素

當一個元件更新時,元件例項會保持不變,但是state或props中資料的變化會呼叫render從而改變該元件中的子元素進行更新。

對比同一型別的元素

當對比兩個相同型別的 React 元素時,React 會保留 DOM 節點,僅比對及更新有改變的屬性。

<div className="before" title="stuff" />

<div className="after" title="stuff" />

React將div的className由before修改為程式設計客棧after(類似於React中更新state的合併操作)。

對子節點進行遞迴

預設情況下(逐層比較),當遞迴 DOM 節點的子元素時,React 會同時遍歷兩個子元素的列表;當產生差異時,生成一個 mutation(突變)。

所以在列表末尾新增元素時,更新開銷比較小。例如:

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React 會先匹配兩個<li>first&l程式設計客棧t;/li>對應的樹,然後匹配第二個元素<li>second</li>對應的樹,最後插入第三個元素的<li>third</li>樹。

如果只是簡單的將新增元素插入到表頭,那麼更新開銷會比較大。比如:

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React 並不會意識到應該保留<li>Duke</li>和<li>Villanova</li>,而是會重建每一個子元素。這種情況會帶來效能問題。

Keys

為了解決上述問題,React 引入了key屬性。當子元素擁有 key 時,React 使用 key 來匹配原有樹上的子元素以及最新樹上的子元素。以下示例在新增key之後,使得樹的轉換效率得以提高:

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

現在 React 知道只有帶著'2014'key 的元素是新元素,帶著'2015'以及'2016'key 的元素僅僅移動了。所以只是建立了 key=2014的元素,並不會建立剩下的兩個元素。

所以key的取值最好不要使用陣列的下標,因為陣列的順序可能會發生變化,最好使用自身資料攜帶的唯一標識(id或是其它的屬性)。

1.虛擬DOM中key的作用:
&nqMfKVgbsp;1).簡單的說:key是虛擬DOM物件的標識,在更新顯示時key起著極其重要的作用。

2).詳細的說:當狀態中的資料發生變化時,react會根據【新資料】生成【新的虛擬DOM】,隨後React進行【新虛擬DOM】與【舊虛擬DOM】的diff比較,比較規則如下:

a.舊虛擬DOM中找到了與新虛擬DOM相同的key:
(1).若虛擬DOM中內容沒變,直接使用之前的真實DOM
(2).若虛擬DOM中內容變了,則生成新的真實DOM,隨後替換掉頁面中之前的真實DOM

b.舊虛擬DOM中未找到與新虛擬DOM相同的key
根據資料建立新的真實DOM,隨後渲染到到頁面

2.用index作為key可能會引發的問題:
1.若對資料進行:逆序新增、逆序刪除等破壞順序操作:
會產生沒有必要的真實DOM更新==>介面效果沒問題,但效率低。

&qMfKVgnbsp;2.如果結構中還包含輸入類的DOM:
&nbsqMfKVgp;會產生錯誤DOM更新==>介面有問題。
3.注意!如果不存在對資料的逆序新增、逆序刪除等破壞順序操作,
    僅用於渲染列表用於展示,使用index作為key是沒有問題的。

以上就是React Diff原理深入分析的詳細內容,更多關於React Diff原理的資料請關注我們其它相關文章!