react更新渲染及渲染原理


一、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

參考文檔:https://zhuanlan.zhihu.com/p/43566956


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM