從 setState 聊到 React 性能優(yōu)化
點(diǎn)擊上方 前端Q,關(guān)注公眾號(hào)
回復(fù)加群,加入前端Q技術(shù)交流群
作者:風(fēng)不識(shí)途
https://segmentfault.com/a/1190000039776687
setState的同步和異步
1.為什么使用setState
開發(fā)中我們并不能直接通過修改 state的值來讓界面發(fā)生更新:因?yàn)槲覀冃薷牧?nbsp; state之后, 希望React根據(jù)最新的Stete來重新渲染界面, 但是這種方式的修改React并不知道數(shù)據(jù)發(fā)生了變化React并沒有實(shí)現(xiàn)類似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式來監(jiān)聽數(shù)據(jù)的變化我們必須通過 setState來告知React數(shù)據(jù)已經(jīng)發(fā)生了變化疑惑: 在組件中并沒有實(shí)現(xiàn) steState方法, 為什么可以調(diào)用呢?原因很簡(jiǎn)單: setState方法是從Component中繼承過來的

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

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

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

驗(yàn)證二: 在原生 DOM事件 —> 同步更新

4.源碼分析

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

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

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

React 更新機(jī)制
1.React 更新機(jī)制
我們?cè)谇懊嬉呀?jīng)學(xué)習(xí) React的渲染流程:

那么 React 的更新流程呢?

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

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

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

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

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

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

React會(huì)對(duì)每一個(gè)子元素產(chǎn)生一個(gè)mutation,而不是保持 <li>星際穿越</li>和<li>盜夢(mèng)空間</li>的不變這種低效的比較方式會(huì)帶來一定的性能問題
React 性能優(yōu)化
1.key的優(yōu)化
我們?cè)谇懊姹闅v列表時(shí),總是會(huì)提示一個(gè)警告,讓我們加入一個(gè) key屬性:

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

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


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

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...



