在前端領域中,接觸到Diff算法基本上是基於如今的前端框架,如React、Vue等,目前的框架基本上都用到了Diff和V-dom,而Diff算法做爲V-dom的加速器,在提升性能方面有極其重要的做用,在每次的update都能高效的渲染新的UI界面,使得你們不用在關心渲染方面上的問題,只須要專一於界面和業務需求;html
前端框架上的Diff算法和傳統的Diff有一點區別,由於框架上有針對使用場景的策略和處理,如下只經過對比和理解傳統Diff算法來去理解如今React的Diff在React中的處理;前端
傳統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);
若是有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的注意點
//父組件 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使用**
簡單點總結就是簡單(Dom,數據)、惟一(key,類型)、穩定(操做dom,列表元素);