一、element如何生成真實DOM節點
觸發組件的更新有兩種更新方式:props以及state改變帶來的更新。本次主要解析state改變帶來的更新。整個過程流程圖如下:
1、一般改變state,都是從setState開始,這個函數被調用之后,會將我們傳入的state放進pendingState的數組里存起來,然后判斷當前流程是否處於批量更新,如果是,則將當前組件的instance放進dirtyComponent里,當這個更新流程中所有需要更新的組件收集完畢之后(這里面涉及到事務的概念,感興趣的可以自己去了解一下)就會遍歷dirtyComponent這個數組,調用他們的uptateComponent對組件進行更新。當然,如果當前不處於批量更新的狀態,會直接去遍歷dirtyComponent進行更新。
2、在我們這個例子中,由於Example是自定義組件,所以調用的是ReactCompositeComponentWrapper這個類的updateComponent方法,這個方法做三件事。
計算出nextState
render()得到nextRenderElement
與prevElement 進行Diff 比較(這個過程后面會介紹),更新節點
最后這個需要去更新節點的時候,跟首次渲染一樣,也需要調用ReactDOMComponent的updateComponent來更新。其中第二步render得到的也是自定義組件的話, 會形成遞歸調用。
接下來,還是上次的問題:那么更新過程中的生命周期函數,shouldComponentUpdate,componentWillUpdate跟componentDidUpdate在哪被調用呢?
shouldComponentUpdate
由圖可知,shouldComponentUpdate在第一步調用得到nextState之后調用,因為nextState也是它的其中一個參數嘛~這個函數很重要,它是我們性能優化的一個很關鍵的點:由圖可以看到,當shouldComponentUpdate返回false的時候,下面的一大塊都不會被去執行,包括已經被優化的diff算法。
當shouldComponentUpdate返回true的時候,會先調用componentWillUpdate,在整個更新過程結束之后調用componentDidUpdate。
以上就是更新渲染的過程。
Diff算法
React基於兩個假設:
兩個相同的組件產生類似的DOM結構,不同組件產生不同DOM結構
對於同一層次的一組子節點,它們可以通過唯一的id區分
發明了一種叫Diff的算法來比較兩棵DOM tree,它極大的優化了這個比較的過程,將算法復雜度從O(n^3)降低到O(n)。
同時,基於第一點假設,我們可以推論出,Diff算法只會對同層的節點進行比較。如圖,它只會對顏色相同的節點進行比較。
也就是說如果父節點不同,React將不會在去對比子節點。因為不同的組件DOM結構會不相同,所以就沒有必要在去對比子節點了。這也提高了對比的效率。
下面,我們具體看下Diff算法是怎么做的,這里分為三種情況考慮
-
節點類型不同
-
節點類型相同
-
子節點比較
不同節點類型
對於不同的節點類型,react會基於第一條假設,直接刪去舊的節點,新建一個新的節點。
比如:
<A>
<C/>
</A>
// 由shape1到shape2<B>
<C/>
</B>
React會直接刪掉A節點(包括它所有的子節點),然后新建一個B節點插入
為了驗證這一點,我打印出了從shape1到shape2節點的生命周期,鏈接如下:
https://codesandbox.io/s/lyop4w9x9mlyop4w9x9m - CodeSandboxlyop4w9x9m - CodeSandbox
最后終端輸出的結果是:
Shape1 :
A is created
A render
C is created
C render
C componentDidMount
A componentDidMountShape2 :
A componentWillUnmount
C componentWillUnmount
B is created
B render
C is created
C render
C componentDidMount
B componentDidMount
由此可以看出,A與其子節點C會被直接刪除,然后重新建一個B,C插入。這樣就給我們的性能優化提供了一個思路,就是我們要保持DOM標簽的穩定性。
打個比方,如果寫了一個<div><List /></div>
(List 是一個有幾千個節點的組件),切換的時候變成了<section><List /></section>
,此時即使List的內容不變,它也會先被卸載在創建,其實是很浪費的。
相同節點類型
當對比相同的節點類型比較簡單,這里分為兩種情況,一種是DOM元素類型,對應html直接支持的元素類型:div,span和p,還有一種是自定義組件。
- DOM元素類型
react會對比它們的屬性,只改變需要改變的屬性
比如:
<div className="before" title="stuff" />
<div className="after" title="stuff" />
這兩個div中,react會只更新className的值
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
這兩個div中,react只會去更新color的值
- 自定義組件類型
由於React此時並不知道如何去更新DOM樹,因為這些邏輯都在React組件里面,所以它能做的就是根據新節點的props去更新原來根節點的組件實例,觸發一個更新的過程,最后在對所有的child節點在進行diff的遞歸比較更新。
- shouldComponentUpdate
- componentWillReceiveProps
- componentWillUpdate
- render
- componentDidUpdate
子節點比較
<div>
<A />
<B />
</div>
// 列表一到列表二<div>
<A />
<C />
<B />
</div>
因為React在沒有key的情況下對比節點的時候,是一個一個按着順序對比的。從列表一到列表二,只是在中間插入了一個C,但是如果沒有key的時候,react會把B刪去,新建一個C放在B的位置,然后重新建一個節點B放在尾部。
我們還是跑一邊代碼,看看生命周期驗證一下,連接地址為:lpl52wy9vl - CodeSandbox
列表一:
A is created
A render
B is created
B render
A componentDidMount
B componentDidMount列表二:
A render
B componentWillUnmount
C is created
C render
B is created
B render
A componentDidUpdate
C componentDidMount
B componentDidMount
當節點很多的時候,這樣做是非常低效的。有兩種方法可以解決這個問題:
1、保持DOM結構的穩定性,我們來看這個變化,由兩個子節點變成了三個,其實是一個不穩定的DOM結構,我們可以通過通過加一個null,保持DOM結構的穩定。這樣按照順序對比的時候,B就不會被卸載又重建回來。
<div>
<A />
{null} <B />
</div>
// 列表一到列表二<div>
<A />
<C />
<B />
</div>
2、key
通過給節點配置key,讓React可以識別節點是否存在。
配上key之后,在跑一遍代碼看看。
A render
C is created
C render
B render
A componentDidUpdate
C componentDidMount
B componentDidUpdate
果然,配上key之后,列表二的生命周期就如我所願,只在指定的位置創建C節點插入。
這里要注意的一點是,key值必須是穩定(所以我們不能用Math.random()去創建key),可預測,並且唯一的。
這里給我們性能優化也提供了兩個非常重要的依據:
-
保持DOM結構的穩定性
-
map的時候,加key