diff算法
vue中v-for中加key 跟這個也很類似
虛擬DOM的兩個假設
1.組件的 DOM 結構是相對穩定的
2.類型相同的兄弟節點可以被唯一標識
然后,我們從react的diff算法開始講起。react有着一套嚴密的算法來確保每次組件的所有變動都能及時的得到更新。這套算法不同於標准的Tree Diff算法,建立在以下兩個假設的基礎上,並將算法復雜度優化到O(n)(標准的Tree Diff算法復雜度為O(n3),意味着如果你的組件中有1000個元素,則需要1000000000次的比對,10億次,這是啥概念嘛,這個性能是無法承受的):
1、不同的元素類型生產不同的虛擬DOM樹。
2、開發人員可以通過不同的key屬性來標識哪些子元素在不同的渲染環境中需要保持穩定的。
第二個假設引申意思就是,如果兩個元素有不同的key,那么在前后兩次渲染中就會被認為是不同的元素,這時候舊的那個元素會被unmount,新的元素會被mount。換言之,如果兩個元素是相同的key,且滿足第一點元素類型相同,則會被認為是兩個相同的元素。這一點不僅僅是適用於在元素遍歷的時候,是整個react diff算法的前提。
例如在我們項目中有這樣一段代碼:
//更新前
render(){
return (
<List key = '1'>
);
}
//更新后
render(){
return (
<List key = '2'>
);
}
這個時候,react就會把這個前一個List銷毀之后重新構建一個List的實例(非必要情況下不要這么做,會有額外的性能開銷)。
為什么我們在遍歷生成元素的時候react要特別警告給元素加上key呢。
遍歷生成元素的時候有什么不同
我們在做遍歷的時候會返回出一組元素類型相同的子元素。這個過程其實並沒有任何問題。問題在於我們對數據進行修改的時候(例如:向數組中插入一個元素,修改一個數組元素的值或則重新排序等),會有很大的不確定性,可能會給react的diff算法帶來災難。因為react在比較元素子元素是否相同的時候並不會精確查找元素具體的位置變動,只會在查到到不同之后對之后所有的元素全部執行一次dom更新操作。
//tree1
<ul>
<li>1</li>
<li>2</li>
</ul>
//tree 2
<ul>
<li>1</li>
<li>3</li>
</ul>
react遇到這種情況的時候,只會修改第二個元素,通過ele.innerHTML = '3'的方法去更新dom,而不會去更新第一個子元素。這種情況下,性能開銷會相對較小。但是如果遇到下面的情況,性能開銷就大了。
//tree1
<ul>
<li>1</li>
<li>2</li>
</ul>
//tree 2
<ul>
<li>1</li>
<li>3</li>
<li>2</li>
</ul>
在上面的例子中我們試圖在tree1中插入一個子元素。這時候react並不會執行插入操作,他直接會移除原先的第二個子元素,然后再append進去剩下的子元素,而其實我們這個操作只只需要一個insert操作就能完成。為了解決這種問題,react需要我們提供給一個key來幫助更新,減少性能開銷。
再談key的作用
在上面的例子中如果我們給每個li元素添加一個key屬性情況就會得到優化。
//tree1
<ul>
<li key='1'>1</li>
<li key='2'>2</li>
</ul>
//tree 2
<ul>
<li key='1'>1</li>
<li key='3'>3</li>
<li key='2'>2</li>
</ul>
這個時候react就會通過key來發現tree2的第二個元素不是原先tree1的第二個元素,原先的第二個元素被挪到下面去了,因此在操作的時候就會直接指向insert操作,來減少dom操作的性能開銷。
我們要如何選擇key
大部分情況下我們要在執行數組遍歷的時候會用index來表示元素的key。這樣做其實並不是很合理。我們用key的真實目的是為了標識在前后兩次渲染中元素的對應關系,防止發生不必要的更新操作。那么如果我們用index來標識key,數組在執行插入、排序等操作之后,原先的index並不再對應到原先的值,那么這個key就失去了本身的意義,並且會帶來其他問題。
例如:數組a=['a','b','c'];這個時候原先的0對應的是'a',1對應的是'b',依次...,如果我們對數組進行依次reverse操作,那么這個時候0就對應成了'c',2變成了'a'。這樣的導致的除了效率問題還可能會產生額外的bug。具體例子可以點擊這里。你可以試着add幾條數據之后在input框中輸入一些值,然后點下order就會后發現問題所在。
所以我們在選擇key的時候一定要選擇能和數據一一對應的值。如果找不到這個值可以參考下面操作。
var key = 0;
//可以是任何的id generator
function id(){
return String(++key);
}
//任意的數組或者待遍歷的數據
data.forEach((item)=>{
if(!item.id){
item.id = id;
}
})
通過上面的方法我們手動給數組的每個元素添加一個唯一的標識id。
幾點提醒
-
key值一定要和具體的元素一一對應到。
-
盡量不要用數組的index去作為key。
-
永遠不要試圖在render的時候用隨機數或者其他操作給元素加上不穩定的key,這樣造成的性能開銷比不加key的情況下更糟糕。
本文出自https://www.jianshu.com/p/a4ac355ab48c
diff算法
vue中v-for中加key 跟這個也很類似
虛擬DOM的兩個假設
1.組件的 DOM 結構是相對穩定的
2.類型相同的兄弟節點可以被唯一標識
然后,我們從react的diff算法開始講起。react有着一套嚴密的算法來確保每次組件的所有變動都能及時的得到更新。這套算法不同於標准的Tree Diff算法,建立在以下兩個假設的基礎上,並將算法復雜度優化到O(n)(標准的Tree Diff算法復雜度為O(n3),意味着如果你的組件中有1000個元素,則需要1000000000次的比對,10億次,這是啥概念嘛,這個性能是無法承受的):
1、不同的元素類型生產不同的虛擬DOM樹。
2、開發人員可以通過不同的key屬性來標識哪些子元素在不同的渲染環境中需要保持穩定的。
第二個假設引申意思就是,如果兩個元素有不同的key,那么在前后兩次渲染中就會被認為是不同的元素,這時候舊的那個元素會被unmount,新的元素會被mount。換言之,如果兩個元素是相同的key,且滿足第一點元素類型相同,則會被認為是兩個相同的元素。這一點不僅僅是適用於在元素遍歷的時候,是整個react diff算法的前提。
例如在我們項目中有這樣一段代碼:
//更新前
render(){
return (
<List key = '1'>
);
}
//更新后
render(){
return (
<List key = '2'>
);
}
這個時候,react就會把這個前一個List銷毀之后重新構建一個List的實例(非必要情況下不要這么做,會有額外的性能開銷)。
為什么我們在遍歷生成元素的時候react要特別警告給元素加上key呢。
遍歷生成元素的時候有什么不同
我們在做遍歷的時候會返回出一組元素類型相同的子元素。這個過程其實並沒有任何問題。問題在於我們對數據進行修改的時候(例如:向數組中插入一個元素,修改一個數組元素的值或則重新排序等),會有很大的不確定性,可能會給react的diff算法帶來災難。因為react在比較元素子元素是否相同的時候並不會精確查找元素具體的位置變動,只會在查到到不同之后對之后所有的元素全部執行一次dom更新操作。
//tree1
<ul>
<li>1</li>
<li>2</li>
</ul>
//tree 2
<ul>
<li>1</li>
<li>3</li>
</ul>
react遇到這種情況的時候,只會修改第二個元素,通過ele.innerHTML = '3'的方法去更新dom,而不會去更新第一個子元素。這種情況下,性能開銷會相對較小。但是如果遇到下面的情況,性能開銷就大了。
//tree1
<ul>
<li>1</li>
<li>2</li>
</ul>
//tree 2
<ul>
<li>1</li>
<li>3</li>
<li>2</li>
</ul>
在上面的例子中我們試圖在tree1中插入一個子元素。這時候react並不會執行插入操作,他直接會移除原先的第二個子元素,然后再append進去剩下的子元素,而其實我們這個操作只只需要一個insert操作就能完成。為了解決這種問題,react需要我們提供給一個key來幫助更新,減少性能開銷。
再談key的作用
在上面的例子中如果我們給每個li元素添加一個key屬性情況就會得到優化。
//tree1
<ul>
<li key='1'>1</li>
<li key='2'>2</li>
</ul>
//tree 2
<ul>
<li key='1'>1</li>
<li key='3'>3</li>
<li key='2'>2</li>
</ul>
這個時候react就會通過key來發現tree2的第二個元素不是原先tree1的第二個元素,原先的第二個元素被挪到下面去了,因此在操作的時候就會直接指向insert操作,來減少dom操作的性能開銷。
我們要如何選擇key
大部分情況下我們要在執行數組遍歷的時候會用index來表示元素的key。這樣做其實並不是很合理。我們用key的真實目的是為了標識在前后兩次渲染中元素的對應關系,防止發生不必要的更新操作。那么如果我們用index來標識key,數組在執行插入、排序等操作之后,原先的index並不再對應到原先的值,那么這個key就失去了本身的意義,並且會帶來其他問題。
例如:數組a=['a','b','c'];這個時候原先的0對應的是'a',1對應的是'b',依次...,如果我們對數組進行依次reverse操作,那么這個時候0就對應成了'c',2變成了'a'。這樣的導致的除了效率問題還可能會產生額外的bug。具體例子可以點擊這里。你可以試着add幾條數據之后在input框中輸入一些值,然后點下order就會后發現問題所在。
所以我們在選擇key的時候一定要選擇能和數據一一對應的值。如果找不到這個值可以參考下面操作。
var key = 0;
//可以是任何的id generator
function id(){
return String(++key);
}
//任意的數組或者待遍歷的數據
data.forEach((item)=>{
if(!item.id){
item.id = id;
}
})
通過上面的方法我們手動給數組的每個元素添加一個唯一的標識id。
幾點提醒
-
key值一定要和具體的元素一一對應到。
-
盡量不要用數組的index去作為key。
-
永遠不要試圖在render的時候用隨機數或者其他操作給元素加上不穩定的key,這樣造成的性能開銷比不加key的情況下更糟糕。
本文出自https://www.jianshu.com/p/a4ac355ab48c
官網 協同章節
設計動力
在某一時間節點調用 React 的 render()方法,會創建一顆由 React 元素組成的樹.在下一次 state 或者 props 更新的時候,相同的 render()方法會返回一棵不同的樹.React需要基於這兩棵樹之間的差別來判斷如何有效率的更新 UI 以保證當前的 UI 與最新的樹保持同步.
目前最牛逼的算法,將一棵樹轉換成另一顆樹的時間復雜度仍然為 O(n^3),倘若 1000 個元素...
Diffing算法
-
對比不同類型的元素
當根節點為不同類型的元素的時候,React 會拆卸原有的樹並建立起新的樹。舉個例子,當一個元素從<a>變成<img>,從<Article>變成<Comment>,或者從<Button>變為<div>都會觸發一個完整的重建流程。
當拆卸一棵數的時候,對應的 DOM 節點也會被銷毀。組件實例將執行 componentWillUnmount()方法。當監理一棵新的樹的時候,對應 DOM 節點會被創建以及插入到 DOM 中。組件實例將執行 componentWillMuount()方法,緊接着 componentDidMount()方法。所有之前的樹所關聯的 state 也會被銷毀。
在根節點一下的組件也會被銷毀,它們的狀態 state 也會被銷毀。比如,當比對以下變更的時候:
<div> <Counter /> </div> <span> <Counter /> </span>
-
對比同一類型的元素
當對比兩個相同類型的 React 元素的時候,React 會保留 DOM 節點,僅對比和更新有改變的屬性
<div className="before" title="stuff"/> <div className="after" title="stuff"/>
對比這兩個元素,React 知道只需要修改 DOM 元素上的 className 屬性。比如:
<div style={{color:'red', fontWeight: 'bold'}} /> <div style={{color:'green', fontWeight: 'bold'}} />
通過對比着兩個元素,React知道需要修改 DOM 元素上的 color 樣式,無需修改 fontWeight
在處理完當前節點之后,React 繼續對子節點進行遞歸
-
對比同類型的組件元素
當一個組件更新的時候,組件實例保持不變。這樣 state 在跨越不同的渲染時保持一致。React 將更新該組件實例的 props 以跟最新的元素保持一致,並且調用該實例的 componentWillReceiveProps() 和 componentWillUpdate()方法。
下一步,調用 render()方法,diff 算法將在之前的結果以及新的結果中進行遞歸
-
對子節點進行遞歸
在默認條件下,當遞歸 DOM 節點的子元素的時候,React 會同時遍歷兩個子元素的列表;當產生差異的時候,生成一個 mutation
在子元素列表末尾新增元素的時候,變更的開銷會變的比較小,比如:
<ul> <li>first</li> <li>second</li> </ul> <ul> <li>first</li> <li>second</li> <li>third</li> </ul>
React會匹配兩個
- first 對應的樹,然后匹配第二個元素
- second 對應的樹,最后插入第三個元素的
- third 樹。
- Duke 和
- Villanova 子樹完成。這種情況下的低效可能會帶來性能問題。
-
keys
為了解決上面的問題,React 支持 key 屬性。當子元素擁有 key 時,React 使用 key 來匹配原有的樹上的子元素以及最新的樹上的子元素。一下例子在新增 key 之后使得之前的低效變為高效:
<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 並不困難,你要展現的元素可能已經有了一個唯一的 ID,於是 key 可以直接從你的數據里面獲取:
<li key={item.id}>{item.name}</li>
當以上的情況不成立的時候,你可以新增一個 ID 字段到你的模型里面,或者利用一部分的內容作為哈希值來生成一個 key。這個 key 不需要全局唯一,但是在列表里面需要保持唯一。
最后,你也可以使用元素在數組當中的下標作為 key。這個策略在元素不進行重新排序的時候比較合適,一旦有順序修改,diff 就會變得很慢。
當基於下表的組件進行重新排序的時候,組件 state 可能就會遇到一些問題。由於組件實例是基於它們的 key 來決定是否更新以及復用,如果一個 key 是一個下標,那么修改順序的時候會修改當前的 key,導致非受控組件的 state(比如輸入框)可能相互篡改導致無法預期的變動。
-
權衡
請謹記協調算法是實現的一個細節。React 可以在每個 action 之后對整個應用進行重新渲染,得到的最終結果也會是一樣的。在此情境下,重新渲染表示在所有組件內調用 render 方法,這並不代表 React 會卸載或者裝載它們。React 只會基於以上提到的規則來決定如何進行差異的合並。
我們定期優化搜索算法,讓常見用例更高效地執行。在當前的實現中,可以理解為一個子樹只能在兄弟之間移動,但是不能移動到其他的位置。這種情況下,算法會重新渲染整顆的子樹。
由於 React 依賴探索的算法,因此當一下假設沒有滿足的時候,性能就會有所損耗。
- 該算法不會嘗試匹配不同組件類型的子樹。如果你發現你在兩種不同類型的組件中切換,但是輸出非常相似的內容,建議把他們改成同一種類型。在實踐中,我們沒有遇到這類的問題。
- Key 應該具有穩定性,可以預測,以及列表內唯一。不穩定的 key(比如通過 Math.random()生成的)會導致許多組件實例和 DOM 節點被不必要地重新創建,這會導致性能下降以及組件中狀態的丟失。
如果簡單實現的話,那么在列表頭部插入會很影響性能,那么更變開銷會比較大。比如:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
React會針對每個子元素 mutate 而不是保持相同的