React原理我的理解-Diff

前言

在前端領域中,接觸到Diff算法基本上是基於如今的前端框架,如React、Vue等,目前的框架基本上都用到了Diff和V-dom,而Diff算法做爲V-dom的加速器,在提升性能方面有極其重要的做用,在每次的update都能高效的渲染新的UI界面,使得你們不用在關心渲染方面上的問題,只須要專一於界面和業務需求;html

前端框架上的Diff算法和傳統的Diff有一點區別,由於框架上有針對使用場景的策略和處理,如下只經過對比和理解傳統Diff算法來去理解如今React的Diff在React中的處理;前端

傳統Diff算法

傳統Diff算法本質是計算一棵樹形結構轉換成另外一棵樹形結構的最少操做,React自己是藉助Virtual DOM+Diff使得它與傳統的Dom操做有了極大的突破,但React的是在傳統上的處理,因此咱們如今先看看傳統的Diff算法;react

傳統的Diff算法,操做包括替換、插入、刪除,在對比時的複雜度爲o(n**3)算法

o(n**3)的由來數組

編輯距離(Edit-Distance)前端框架

計算 字符串neigevor與Neigevoir的編輯距離(3)
n -> N Neigevor
r -> i Neigevoi
add r Neigevoirapp

同理對於dom這類的對象來講也是同樣,由替換、插入、刪除,但相對來講比只只有Edit-Distance複雜;框架

例如:dom

before                 after
      A                     A  
    /   \                 /   \
   B     D     =>        B     D
  /                             \
 C                               E
                                  \
                                   C

B delete C
D add E
D add C 組件化

直觀感覺雖然只是作了3步,可是對於計算機來講須要對兩個樹進行遍歷;
可能有人有點好奇遍歷的方式,我我的是這麼理解的,先找到兩個樹的差別再進行轉換操做;

一、爲什麼非要遍歷兩個樹,不能只以before來遍歷,不就o(n)?
若是直接before來遍歷,遇到B刪除C,D的時候增長E-C,看起來的確是,但after的可能性是無限的,若是變成:

before                after
      A                     E  
    /   \                 /   \
   B     D     =>        A     F
  /                     / \
 C                     B   D
                      /
                     C

再以before來遍歷,這樣遍歷的時候直接就變成直接生成整個after結果,這樣不違背了Diff的原則(最少操做);

二、如下圖爲例,以直觀來看只要生成一個E,而後將before放進去,而後再加一個G和F便可:
即使如此也是須要兩個樹嵌套遍歷進行查詢

before                after
      A                     E  
    /   \                 /   \
   B     D     =>        A     F
  /                     / \
 C                     B   D
                      /
                     C
                    /
                   G

以A爲例,是否A須要遍歷完整整個after
1.在after中遇到A,break去執行B的遍歷,可能出現遍歷到C的時候,C-G這樣的狀況,break就沒法清楚G節點;
2.只判斷父子節點,難保F一邊可能也出現before;

因此當須要進行獲取不一樣的節點時就須要o(n**2)的時間複雜度;

三、在獲取到了差別後進行操做,多少個差別處理多少次難道不是o(n2)+o(n)或者在邊找的時候邊處理直接o(n2)?

第一反應是這個感受,但後來想一想是和Edit Distance相關;(我的理解)

仍是以A爲例:

一、在遍歷E的時候,由於A的父節點爲Null,E的也是Null,而且A!==E因此直接生成;
二、遍歷到A的時候,存在Edit的問題,須要清楚是刪除或是插入等操做;
三、因此須要遍歷A的下一層子節點,發現都是B-D,因此A不用操做;
四、類推,當before的C在after中遍歷到C的時候,一樣須要看子節點,因此插入一個G;

因此是O(n**3);

簡單點的理解就是時找到差別o(n2),而後尋找差別進行edit的是o(n),因此是o(n3);

React的Diff

若是有100個元素,在一次改變後就須要1000000的計算量,這對於前端來講確定爆炸,
但100個元素在前端來講也不是那麼罕見,例如列表等;因此直接用傳統的Diff來去處理意義不是特別大,
可能還不必定有直接從新渲染來的直接,須要在傳統的Diff上進行改造;

在不看React官方的優化前,其實咱們也在瞭解Diff的過程當中也遇到和理解的優化點;

1.在遍歷查詢差別節點時,只用一個做爲基礎O(n),上文說了會致使所有渲染,但實際這樣的狀況極少數發生;
    A、由於Dom的處理不多跨層級去移動,大部分都是appendChild、removeChild或者改變Child的內容;
    B、組件化,使得Dom更容易同級比對,不用考慮跨級;
  
2.便是減小一層遍歷,也只是從o(n3)變成o(n2),因此還須要在操做節點的方面上處理,不用在遍歷查找差別;
    A、由於同級比對,因此不進行對最優操做進行遍歷,只須要考慮是否是該節點有改變;
    B、優化遍歷過程當中,部分並未進行改變的節點,能夠不用再向下遍歷;

目前來看已經有了很多改進,可是當父節點不變,子節點位置改變時,按上面說的會直接刪除生成,也並非好的處理方式;

before                after
      A                     A  
    /   \                 /   \
   B     C     =>        C     B

最終其實須要作成的就是直接移動位置便可,下面有說到解決方法;

因此咱們再來看看,React官方的假設基礎:

一、兩個不一樣類型的元素會產生出不一樣的樹;
二、開發者能夠經過 key prop 來暗示哪些子元素在不一樣的渲染下能保持穩定;

最終React的策略以下:

同層級對比(Tree Diff)

只對比先後樹同層級的差別,直接新增或者刪除;

 before                     after
      A                     D 
    /   \                 /   
   B     C     =>        A
                        / \       
                       B   C
                       
after裏面的元素所有都從新生成;

比對不一樣類型的元素(Component Diff)

若是節點爲不一樣類型的元素時,直接進行刪除而後從新建立新的元素,進而它的子節點即便和原先的子節點是同樣也須要從新建立;
before                     after
<ConponentA />             <ConponentB />

ConponentA與ConponentB的元素皆爲<div>test</div>,也是須要從新生成;

比對同一類型的元素(Element Diff)

before                     after
<ConponentA />             <ConponentA />

在先後元素類型相同後,進行ElementDiff的對比;

一、當節點爲相同類型的元素時,若是隻是改變屬性子節點沒有任何變化時,僅比對及更新有改變的屬性;
   <div className="before" title="stuff" /> ===> <div className="after" title="stuff" />
   因此只須要修改 DOM 元素上的 className 屬性
   
二、當節點爲相同類型的元素時,子節點有變化,如位置、數目等,則會進行插入、刪除、移動等操做;;
   before                     after
   <div>                       <div>
       <span>1</span>            <span>1</span>
       <span>2</span>            <span>2</span>
   </div>                        <span>3</span>
                               </div>
   在遍歷完1和2的對比以後插入一個3便可;

key的重要性
key能夠理解爲身份標誌,因此必需要保持惟一和穩定;

一、防止錯誤的操做,減小性能消耗;

before                         after
   <div>                           <div>
       <span key="1">1</span>          <span key="3">3</span>
       <span key="2">2</span>          <span key="1">1</span>
   </div>                              <span key="2">2</span>
                                   </div>
 
經過key能夠發現1和2節點都是相同的節點,只須要移動插入3而後移動一、2便可;
避免以前文中說的的問題,刪除從新而不是移動;

二、減小Diff時間

React是不會給組件增長Key,可是增長key屬性的節點,組件實例會基於它們的key來決定是否更新以及複用,
在key相等的時候會認爲同一元素,不須要進行過多的diff,只須要判斷屬性是否變化,而key不一樣則會銷燬從新生成;

key的注意點

官方Demo

//父組件
state = [{id: 1}]
<div>
    <button onClick={setState([{id:2}, {id: 1})]} >在列表頭插入元素</button>
    {
        state.map((value,inedx) => (<Item id={value.id} key={index} />))
    }
</div>

//Item組件
<div>
    <span>{props.id}</span>
    <input type="text" />
</div>

當用戶在id:1的input輸入value(test)後,再去點擊按鈕插入元素時,會出如今id:1的input輸入值text出如今了id:2的input中

before                         after
    <Item id="1" key="0" />        <Item id="2" key="0" />
                                   <Item id="1" key="1" />
                                  
緣由值由於key,key相等致使id:1和id:2當成相等,因此進行屬性判斷,
因此before的Item只修改了props.id,而不是在前面插入一個Item,只是1變成2,再加一個1;

**若是key是數組下標,那麼修改順序時會修改當前的key,致使非受控組件的state(好比輸入框)
可能相互篡改致使沒法預期的變更。因此在一些數組遍歷的時候不建議用index當key使用**

Diff注意點

  1. 減小直接對原生Dom的操做,保證Dom結構的穩定;(Tree Diff)
  2. 常出現的元素,只作Class的隱藏顯示,不直接影響Dom結構處理;(Tree Diff)
  3. 避免Dom節點過多,可使用靈活使用<></>、Css等處理;(Tree Diff)
  4. 儘可能處理好子元素相同的組件,實現同類型的擴展;(Component Diff)
  5. 處理好key,確保穩定並且惟一;(Element Diff)
  6. 處理好shouldComponentUpdate,減小被動diff;(Element Diff)
  7. 處理好數組等大數據顯示,如1000條時只顯示能顯示的數目既能夠,不須要一次顯示;(如react-virtualized);
  8. 減小列表元素中,元素移動過多;(Element Diff)

簡單點總結就是簡單(Dom,數據)、惟一(key,類型)、穩定(操做dom,列表元素);

相關文章
相關標籤/搜索