React的虛擬Dom和其diff算法,是React渲染效率遠遠高於傳統dom操做渲染效率的主要緣由。一方面,虛擬Dom的存在,使得在操做Dom時,再也不直接操做頁面Dom,而是對虛擬Dom進行相關操做運算。再經過運算結果,結合diff算法,得出變動過的部分Dom,進行局部更新。另外一方面,當存在十分頻繁的操做時,會進行操做的合併。直接在運算出最終狀態以後才進行Dom的更新。從而大大提升Dom的渲染效率。
對於React如何經過diff算法來對比出作出變更的Dom,React內部有着複雜的運算過程,此文不作具體代碼層級的討論。僅僅經過一個小小Demo來宏觀上的探討下diff的運算思路。javascript
React的diff對比是採用深度遍歷的規則進行遍歷比對的。如下圖的Dom結構爲例:
對比過程爲:對比組件1(沒有變化)-> 對比組件2(沒有變化)-> 對比組件4(沒有變化)-> 對比組件5(組件5被移除,記錄一個移除操做)-> 對比組件3(沒有變化)->對比組件3子組件(新增了一個組件5,記錄一個新增操做)。
對比結束,此時變更數據記錄了兩個節點的變更,在渲染時,便會執行一次組件5的移除,和一次組件5的新增。其它節點不作變動,從而實現頁面Dom的更新操做。html
接下來,咱們設計一個簡單的demo,來分析頁面變化時的整個過程。
首先咱們建立幾個相同的Demo
組件:java
import React, { Component } from 'react'; export default class Demo1 extends Component { componentWillMount() { console.log('加載組件1'); } componentWillUnmount() { console.log('銷燬組件1') } render () { return <div>{this.props.children}</div> } }
組件除了將其內部的Dom
直接渲染以外,還在組件加載前和卸載前分別在控制檯中打印出日誌。
接下來經過代碼組合出上圖中的組件結構,並經過事件觸發組件結構的變化。react
// 變化前 <Demo1>1 <Demo2>2 <Demo4>4</Demo4> <Demo5>5</Demo5> </Demo2> <Demo3>3</Demo3> </Demo1> // 變化後 <Demo1>1 <Demo2>2 <Demo4>4</Demo4> </Demo2> <Demo3>3 <Demo5>5</Demo5> </Demo3> </Demo1>
執行變動操做以後,控制檯會打印出日誌算法
加載組件5 銷燬組件5
結果通分析中同樣,分別執行了一次組件5的加載操做和一次組件5的卸載操做。
接下來來分析一些複雜的狀況。
首先看下面這種Dom的刪除
按照前面的分析,比對過程爲:對比組件1(沒有變化)-> 對比組件2(沒有變化)-> 對比組件4(組件4被移除,記錄一個移除操做)-> 對比組件5(沒有變化)-> 對比組件6(沒有變化)-> 對比組件3(沒有變化)
。對比結束。按照這個分析,用代碼進行測試後,控制檯日誌應該會輸出:數組
銷燬組件4
這一條日誌。然而,在實際測試後,會發現輸出日誌爲:dom
加載組件5 加載組件6 銷燬組件4 銷燬組件5 銷燬組件6
能夠發現,除了「銷燬組件4」這一個操做以外,還進行了組件5和組件6的銷燬和加載操做。難道是咱們以前的分析是錯誤的?別急,咱們再來進行另一個實驗:
一樣只刪除了一個組件,只是刪除的組件位置不一樣,按照上次的實驗結果,控制檯輸出日誌應該爲:測試
加載組件4 加載組件5 銷燬組件4 銷燬組件5 銷燬組件6
然而,實際的實驗結果又出乎咱們的預料。實際輸出結果僅爲:this
銷燬組件6
這個現象十分有趣。僅僅是刪除了不一樣位置的組件,diff分析的過程卻徹底不同。其實,若是你繼續實驗刪除組件5,你會發現,所得的結果跟前兩次也是徹底不一樣。
其實diff算法在進行虛擬Dom的變動比對時,並不能精確的進行一對一的比對(固然react提供瞭解決方案,後面討論)。當一個父節點發生變動時,會銷燬掉其下全部的子節點。而其兄弟節點,則會按照節點順序進行一對一的順序比對。那麼在上面第一個例子的比對順序實際上是這樣的:對比組件1(沒有變化)-> 對比組件2(沒有變化)-> 對比組件4(組件4變動爲組件5,記錄一次組件4的移除操做和一次組件5的新增操做)->對比組件5(組件5變動爲組件6,記錄一次組件5的移除操做和一次組件6的新增操做)->對比組件6(組件6被移除,記錄一次組件6的移除操做)
。對比結束。按照這個分析思路,控制檯的輸出結果就不難理解了。
一樣當咱們在第二個例子中移除組件6時。組件4和組件5的順序並無變化,因此對比時,仍然是跟自身組件的虛擬Dom進行比對,沒有變化,因此也就只有一次組件6的移除操做。
咱們能夠進一步經過新增及修改操做來進一步驗證猜測。
經過在組件4前新增一個組件和在組件6後新增一個組件的對比。能夠發現結果與咱們的猜測結果徹底一致。具體實驗推演過程,此處不在贅述。
對於修改,因爲修改並未改變該組件及其兄弟組件的個數及順序,因此僅僅會執行替換組件及其子組件的新增操做和被替換組件的移除操做。
同級的組件分析完了,那麼若是是跨層級的組件操做呢?好比下面這種dom變動:
這種變動,因爲組件2,組件4,組件5三個組件的結構均未有任何變化,那麼會不會複用其整個結構,只進行相對位置的變動呢?實驗發現,控制檯日誌輸出爲:spa
加載組件3 加載組件2 加載組件4 加載組件5 銷燬組件2 銷燬組件4 銷燬組件5 銷燬組件3
可見組件2及其子組件發生變化時,組件2以及其下的全部子組件均會被從新渲染。那麼爲何組件3也會從新渲染呢?其實緣由並非其增長了子節點,而是由於其兄弟節點2被移除,影響了其相對位置而形成的。其完整的對比流程爲:對比組件1(沒有變化)-> 對比組件2(組件二變動爲組件3,記錄一次組件2的移除操做以及其子組件:組件4和組件5的移除操做,同時記錄組件3的新增操做,以及其子組件:組件2,組件4和組件5的移除操做)-> 對比組件3(組件3被移除,記錄一次組件3的移除操做
分析可見:當一個節點變化時,其下的全部子節點會所有被從新渲染。好比在上個例子中,不進行結構的變動,只是將組件2替換爲組件6,組件4和組件5保持不變,但因爲組件4和組件5是組件2的子組件,組件2的變動依然會致使組件4和組件4被從新渲染。
此外,分析輸出的結果,能夠看到,react在進行局部Dom的更新時,會先執行新組件的加載,再執行組件的移除操做。
在咱們之前的開發工做中,確定遇到過列表的渲染。此時React
會強制咱們爲列表的每一條數據設置一個惟一的key
值(不然控制檯會報警告),而且官方禁止使用列表數據的下標來做爲key值。在React 16
及之後版本中,新增的以數組的形式來渲染多個同級的兄弟節點的寫法中,一樣要求咱們爲每一項添加惟一key值。你可能很疑惑這個必須加的key
,彷佛並無什麼實質的做用,爲什麼倒是一個必加項。
其實,在React
進行diff運算時,key
值是十分關鍵的,由於每個key就是該虛擬Dom節點的身份證,在咱們以前的實驗中,因爲沒有定義key
值,diff運算在進行虛擬Dom的比對時,並不知道這個虛擬Dom跟以前的哪一個虛擬Dom是同樣的,因此只能採用順序比對的方案,進行一對一比對。因此纔有了以前分析中的因爲位置的不一樣,致使了徹底不一樣的輸出結果。而當咱們爲每個組件添加key
值以後,因爲有了惟一標示,在進行diff運算時,便能進行精確的比對,再也不受到位置變更的影響。
回到最初的刪除實驗,爲每個組件添加上惟一的key:
// 變化前 <Demo1 key={1}>1 <Demo2 key={2}>2 <Demo4 key={4}>4</Demo4> <Demo5 key={5}>5</Demo5> <Demo6 key={6}>6</Demo6> </Demo2> <Demo3 key={3}>3</Demo3> </Demo1> // 變化後 <Demo1 key={1}>1 <Demo2 key={2}>2 <Demo4 key={4}>4</Demo4> <Demo5 key={5}>5</Demo5> <Demo6 key={6}>6</Demo6> </Demo2> <Demo3 key={3}>3</Demo3> </Demo1>
運行發現,其輸出日誌正是咱們最初設想的那樣:
銷燬組件4
相對於沒有key值的操做,避免了組件5和組件6的從新渲染。大大提升了渲染的效率。此時,爲何列表類數據必須加一個惟一的key值,就顯而易見了。試想一下在一個無限滾動的移動端列表頁面,加載了1000條數據。此時將第一條刪除,那麼,在沒有key值的狀況下,要從新渲染這個列表,須要將第一條以後的999條數據所有從新渲染。而有了key值,僅僅只須要對第一條數據進行一次移除操做就能夠完成。可見,key值對渲染效率的提高,絕對是巨大的。
那麼,爲何不能將key值設置爲數據的下標呢?其實很簡單,由於下標都是從0開始的,仍是這個移動端的列表,刪除了第一條數據,若是將key值設置爲了數據下標。那麼原來的key值爲1的數據,在從新渲染後,key值會從新被設置爲0,那麼在進行比對時,會把這條數據跟變動前的key爲0的數據進行比對,很明顯,這兩條數據並非同一條,因此依然會由於數據不一樣,而致使整個列表的從新渲染。
除此以外,還有一個開發中的共識,就是key值必須惟一。但key值真的不能相同嗎?
按照以前的實驗以及分析,能夠看出:當在進行兄弟節點的比對時,key值可以做爲惟一的標示進行精確的比對。可是對於非兄弟組件,因爲diff運算採用的是深度遍歷,且父組件的變更會徹底更新子組件,因此理論上key值對於非兄弟組件的做用,就顯得微乎其微。那麼對於非兄弟組件,key值相同應該是可行的。那麼用實驗驗證一下咱們的猜測。
// 變動前 <Demo1 key={1}>1 <Demo2 key={1}>2 <Demo4 key={4}>4</Demo4> <Demo5 key={5}>5</Demo5> <Demo6 key={6}>6</Demo6> </Demo2> <Demo3 key={2}>3 <Demo4 key={4}>4</Demo4> <Demo5 key={5}>5</Demo5> <Demo6 key={6}>6</Demo6> </Demo3> </Demo1> // 變動後 <Demo1 key={1}>1 <Demo2 key={1}>2 <Demo5 key={5}>5</Demo5> <Demo6 key={6}>6</Demo6> </Demo2> <Demo3 key={2}>3 <Demo4 key={4}>4</Demo4> <Demo6 key={6}>6</Demo6> </Demo3> </Demo1>
在這個實驗中,組件1和組件2有着相同的key值,且組件2和組件3的子組件也有着相同的key值,然而運行該代碼,卻並無關於key值相同的警告。執行Dom變動後,日誌輸出也同以前的猜測沒有出入。可見咱們的猜測是正確的,key值並不是須要絕對惟一,只是須要保證在同一個父節點下的兄弟節點中惟一即可以了。
除了上面提到的這些以外,在瞭解了key的做用機制以後,還能夠利用key值來實現一些其它的效果。好比能夠利用key值來更新一個擁有自狀態的組件,經過修改該組件的key值,即可以達到使該組件從新渲染到初始狀態的效果。此外,key值除了在列表中使用以外,在任何會操做dom,好比新增,刪除這種影響兄弟節點順序的狀況,均可以經過添加key值的方法來提升渲染的效率。