react渲染原理深度解析


https://mp.weixin.qq.com/s/aM-SkTsQrgruuf5wy3xVmQ   原文件地址

 

【第1392期】React從渲染原理到性能優化(二)-- 更新渲染

黃瓊 前端早讀課 今天

前言

沒去2018 React Conf的童鞋,別錯誤今天的。今日早讀文章由騰訊IMWeb@黃瓊授權分享。

@黃瓊,騰訊前端工程師,IMWeb團隊成員,目前負責企鵝輔導

正文從這開始~~

很多人都使用過React,但是很少人能說出它內部的渲染原理。有人會說,會用就行了,知道渲染原理有必要么?其實渲染原理決定着性能優化的方法,只有在了解原理之后,才能完全理解為什么這樣做可以優化性能。正所謂:知其然,然后知其所以然。

廢話不多說,下面我們就開始吧~

本篇文章,將會分為四部分介紹:

JSX如何生成element

當我們寫下一段JSX代碼的時候,react是如何根據我們的JSX代碼來生成虛擬DOM的組成元素element的。

element如何生成真實DOM節點

再生成elment之后,react又如何將其轉成瀏覽器的真實節點。這里會通過介紹首次渲染以及更新渲染的流程來幫助大家理解這個渲染流程,

性能優化

結合渲染原理,通過實際例子,看看如何優化組件。

React 16異步渲染方案

到目前為止,這些優化組件的方法還不能解決什么問題,所以我們需要引入異步渲染,以及異步渲染的原理是什么。

二、element如何生成真實DOM節點

【第1386期】React從渲染原理到性能優化(一)介紹了首次渲染的過程,接下來我們來看更新渲染的過程。

觸發組件的更新有兩種更新方式: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算法。

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://facebook.github.io/react/docs/reconciliation.htm

關於本文

作者:@黃瓊
原文:
https://zhuanlan.zhihu.com/p/43566956


免責聲明!

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



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