【React】942- 從 setState 聊到 React 性能優(yōu)化

者:風不識途
https://segmentfault.com/a/1190000039776687
setState的同步和異步
1.為什么使用setState
開發(fā)中我們并不能直接通過修改 state的值來讓界面發(fā)生更新:因為我們修改了 state之后, 希望React根據(jù)最新的Stete來重新渲染界面, 但是這種方式的修改React并不知道數(shù)據(jù)發(fā)生了變化React并沒有實現(xiàn)類似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式來監(jiān)聽數(shù)據(jù)的變化我們必須通過 setState來告知React數(shù)據(jù)已經(jīng)發(fā)生了變化疑惑: 在組件中并沒有實現(xiàn) steState方法, 為什么可以調(diào)用呢?原因很簡單: setState方法是從Component中繼承過來的

2.setState異步更新
setState是異步更新的 
為什么 setState設(shè)計為異步呢?setState設(shè)計為異步其實之前在GitHub上也有很多的討論React核心成員(Redux的作者)Dan Abramov也有對應(yīng)的回復, 有興趣的可以看一下 簡單的總結(jié): setState設(shè)計為異步, 可以顯著的提高性能如果每次調(diào)用 setState都進行一次更新, 那么意味著render函數(shù)會被頻繁的調(diào)用界面重新渲染, 這樣的效率是很低的最好的方法是獲取到多個更新, 之后進行批量更新 如果同步更新了 state, 但還沒有執(zhí)行render函數(shù), 那么state和props不能保持同步state和props不能保持一致性, 會在開發(fā)中產(chǎn)生很多的問題
3.如何獲取異步的結(jié)果
如何獲取 setState異步更新state后的值?方式一: setState的回調(diào)setState接收兩個參數(shù): 第二個參數(shù)是回調(diào)函數(shù)(callback), 這個回調(diào)函數(shù)會在state更新后執(zhí)行

方式二: componentDidUpdate生命周期函數(shù)

3.setState一定是異步的嗎?
其實可以分成兩種情況 在組件生命周期或React合成事件中, setState是異步的在 setTimeou或原生DOM事件中,setState是同步的
驗證一: 在 setTimeout中的更新 —> 同步更新

驗證二: 在原生 DOM事件 —> 同步更新

4.源碼分析

setState的合并
1.數(shù)據(jù)的合并
通過 setState去修改message,是不會對其他state中的數(shù)據(jù)產(chǎn)生影響的源碼中其實是有對 原對象 和 新對象 進行合并的

2.多個state的合并
當我們的多次調(diào)用了 setState, 只會生效最后一次state

setState合并時進行累加: 給setState傳遞函數(shù), 使用前一次state中的值

React 更新機制
1.React 更新機制
我們在前面已經(jīng)學習 React的渲染流程:

那么 React 的更新流程呢?

React基本流程
2.React 更新流程
React在props或state發(fā)生改變時,會調(diào)用React的render方法,會創(chuàng)建一顆不同的樹React需要基于這兩顆不同的樹之間的差別來判斷如何有效的更新UI:如果一棵樹參考另外一棵樹進行完全比較更新, 那么即使是最先進的算法, 該算法的復雜程度為 O(n 3 ^3 3),其中 n 是樹中元素的數(shù)量
如果在 React中使用了該算法, 那么展示1000個元素所需要執(zhí)行的計算量將在十億的量級范圍這個開銷太過昂貴了, React的更新性能會變得非常低效
于是,
React對這個算法進行了優(yōu)化,將其優(yōu)化成了O(n),如何優(yōu)化的呢?同層節(jié)點之間相互比較,不會跨節(jié)點比較
不同類型的節(jié)點,產(chǎn)生不同的樹結(jié)構(gòu)
開發(fā)中,可以通過key來指定哪些節(jié)點在不同的渲染下保持穩(wěn)定

情況一: 對比不同類型的元素
當節(jié)點為不同的元素,React會拆卸原有的樹,并且建立起新的樹:
當一個元素從
<a>變成<img>,從<Article>變成<Comment>,或從<button>變成<div>都會觸發(fā)一個完整的重建流程當卸載一棵樹時,對應(yīng)的
DOM節(jié)點也會被銷毀,組件實例將執(zhí)行componentWillUnmount()方法當建立一棵新的樹時,對應(yīng)的
DOM節(jié)點會被創(chuàng)建以及插入到DOM中,組件實例將執(zhí)行componentWillMount()方法,緊接著componentDidMount()方法比如下面的代碼更改:
React 會銷毀 Counter 組件并且重新裝載一個新的組件,而不會對Counter進行復用

情況二: 對比同一類型的元素
當比對兩個相同類型的 React 元素時,React 會保留 DOM 節(jié)點,僅對比更新有改變的屬性 比如下面的代碼更改: 通過比對這兩個元素, React知道只需要修改DOM元素上的className屬性

比如下面的代碼更改:
當更新
style屬性時,React僅更新有所改變的屬性。通過比對這兩個元素,
React知道只需要修改DOM元素上的color樣式,無需修改fontWeight

如果是同類型的組件元素:
組件會保持不變,
React會更新該組件的props,并且調(diào)用componentWillReceiveProps()和componentWillUpdate()方法下一步,調(diào)用
render()方法,diff算法將在之前的結(jié)果以及新的結(jié)果中進行遞歸
情況三: 對子節(jié)點進行遞歸
在默認條件下,當遞歸
DOM節(jié)點的子元素時,React會同時遍歷兩個子元素的列表;當產(chǎn)生差異時,生成一個mutation我們來看一下在最后插入一條數(shù)據(jù)的情況:??

前面兩個比較是完全相同的,所以不會產(chǎn)生mutation
最后一個比較,產(chǎn)生一個mutation,將其插入到新的DOM樹中即可
但是如果我們是在前面插入一條數(shù)據(jù):

React會對每一個子元素產(chǎn)生一個mutation,而不是保持 <li>星際穿越</li>和<li>盜夢空間</li>的不變這種低效的比較方式會帶來一定的性能問題
React 性能優(yōu)化
1.key的優(yōu)化
我們在前面遍歷列表時,總是會提示一個警告,讓我們加入一個 key屬性:

方式一:在最后位置插入數(shù)據(jù)
這種情況,有無 key意義并不大方式二:在前面插入數(shù)據(jù)
這種做法,在沒有 key的情況下,所有的<li>都需要進行修改在下面案例: 當子元素 (這里的
li元素) 擁有key時React使用key來匹配原有樹上的子元素以及最新樹上的子元素:下面這種場景下, key為 111 和 222 的元素僅僅進行位移,不需要進行任何的修改
將
key為333的元素插入到最前面的位置即可
key的注意事項:
key應(yīng)該是唯一的key不要使用隨機數(shù)(隨機數(shù)在下一次render時,會重新生成一個數(shù)字)使用 index作為key,對性能是沒有優(yōu)化的
2.render函數(shù)被調(diào)用
我們使用之前的一個嵌套案例:
在App中,我們增加了一個計數(shù)器的代碼 當點擊
+1時,會重新調(diào)用App的render函數(shù)而當 App 的 render函數(shù)被調(diào)用時,所有的子組件的 render 函數(shù)都會被重新調(diào)用

那么,我們可以思考一下,在以后的開發(fā)中,我們只要是修改 了App中的數(shù)據(jù),所有的子組件都需要重新 render,進行diff算法,性能必然是很低的:事實上,很多的組件沒有必須要重新 render它們調(diào)用 render 應(yīng)該有一個前提,就是依賴的數(shù)據(jù)(state、 props) 發(fā)生改變時,再調(diào)用自己的 render方法如何來控制 render方法是否被調(diào)用呢?通過 shouldComponentUpdate方法即可
3.shouldComponentUpdate
React給我們提供了一個生命周期方法shouldComponentUpdate(很多時候,我們簡稱為SCU),這個方法接受參數(shù),并且需要有返回值;主要作用是:**控制當前類組件對象是否調(diào)用render**方法
該方法有兩個參數(shù): 參數(shù)一: nextProps修改之后, 最新的porps屬性參數(shù)二: nextState修改之后, 最新的state屬性該方法返回值是一個 booolan 類型 返回值為 true, 那么就需要調(diào)用render方法返回值為 false, 那么不需要調(diào)用render方法比如我們在App中增加一個 message屬性:JSX中并沒有依賴這個message, 那么它的改變不應(yīng)該引起重新渲染但是通過 setState修改state中的值, 所以最后render方法還是被重新調(diào)用了
// 決定當前類組件對象是否調(diào)用render方法
// 參數(shù)一: 最新的props
// 參數(shù)二: 最新的state
shouldComponentUpdate(nextProps, nextState) {
// 默認是: return true
// 不需要在頁面上渲染則不調(diào)用render函數(shù)
return false
}
4.PureComponent
如果所有的類, 我們都需要手動來實現(xiàn) shouldComponentUpdate, 那么會給我們開發(fā)者增加非常多的工作量我們設(shè)想一下在 shouldComponentUpdate中的各種判斷目的是什么?props或者state中數(shù)據(jù)是否發(fā)生了改變, 來決定shouldComponentUpdate返回true或false事實上 React已經(jīng)考慮到了這一點, 所以React已經(jīng)默認幫我們實現(xiàn)好了, 如何實現(xiàn)呢?將 class 繼承自 PureComponent 內(nèi)部會進行淺層對比最新的 state和porps, 如果組件內(nèi)沒有依賴porps或state將不會調(diào)用render解決的問題: 比如某些子組件沒有依賴父組件的 state或props, 但卻調(diào)用了render函數(shù)


5.shallowEqual方法
這個方法中,調(diào)用
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState),這個shallowEqual就是進行淺層比較:
6.高階組件memo
函數(shù)式組件如何解決
render: 在沒有依賴state或props但卻重新渲染render問題我們需要使用一個高階組件
memo:我們將之前的Header、Banner、ProductList都通過 memo 函數(shù)進行一層包裹
Footer沒有使用 memo 函數(shù)進行包裹;
最終的效果是,當
counter發(fā)生改變時,Header、Banner、ProductList的函數(shù)不會重新執(zhí)行,而 Footer 的函數(shù)會被重新執(zhí)行
import React, { PureComponent, memo } from 'react'
// MemoHeader: 沒有依賴props,不會被重新調(diào)用render渲染
const MemoHeader = memo(function Header() {
console.log('Header被調(diào)用')
return <h2>我是Header組件</h2>
})

回復“加群”與大佬們一起交流學習~
點擊“閱讀原文”查看 120+ 篇原創(chuàng)文章

