setState 詳解與 React 性能優化
setState 的同步和異步
1. 爲什麼使用 setState
-
開發中我們並不能直接通過修改
state
的值來讓界面發生更新:- 因爲我們修改了
state
之後, 希望React
根據最新的Stete
來重新渲染界面, 但是這種方式的修改React
並不知道數據發生了變化 React
並沒有實現類似於Vue2
中的Object.defineProperty
或者Vue3
中的Proxy
的方式來監聽數據的變化- 我們必須通過
setState
來告知React
數據已經發生了變化
- 因爲我們修改了
-
疑惑: 在組件中並沒有實現
steState
方法, 爲什麼可以調用呢?- 原因很簡單:
setState
方法是從Component
中繼承過來的
- 原因很簡單:
2.setState 異步更新
-
爲什麼
setState
設計爲異步呢?setState
設計爲異步其實之前在GitHub
上也有很多的討論- React 核心成員(Redux 的作者)Dan Abramov 也有對應的回覆, 有興趣的可以看一下
-
簡單的總結:
setState
設計爲異步, 可以顯著的提高性能- 如果每次調用
setState
都進行一次更新, 那麼意味着render
函數會被頻繁的調用界面重新渲染, 這樣的效率是很低的 - 最好的方法是獲取到多個更新, 之後進行批量更新
- 如果每次調用
-
如果同步更新了
state
, 但還沒有執行render
函數, 那麼state
和props
不能保持同步state
和props
不能保持一致性, 會在開發中產生很多的問題
3. 如何獲取異步的結果
-
如何獲取
setState
異步更新state
後的值? -
方式一:
setState
的回調setState
接收兩個參數: 第二個參數是回調函數 (callback
), 這個回調函數會在state
更新後執行
- 方式二:
componentDidUpdate
生命週期函數
3.setState 一定是異步的嗎?
- 其實可以分成兩種情況
- 在組件生命週期或 React 合成事件中,
setState
是異步的- 在
setTimeou
或原生 DOM 事件中,setState
是同步的
- 驗證一: 在
setTimeout
中的更新 —> 同步更新
- 驗證二: 在原生
DOM
事件 —> 同步更新
4. 源碼分析
setState 的合併
1. 數據的合併
-
通過
setState
去修改message
,是不會對其他state
中的數據產生影響的- 源碼中其實是有對 原對象 和 新對象 進行合併的
2. 多個 state 的合併
- 當我們的多次調用了
setState
, 只會生效最後一次state
setState
合併時進行累加: 給 setState 傳遞函數, 使用前一次state
中的值
React 更新機制
1.React 更新機制
- 我們在前面已經學習
React
的渲染流程:
- 那麼 React 的更新流程呢?
2.React 更新流程
-
React
在props
或state
發生改變時,會調用React
的render
方法,會創建一顆不同的樹 -
React
需要基於這兩顆不同的樹之間的差別來判斷如何有效的更新UI
: -
如果一棵樹參考另外一棵樹進行完全比較更新, 那麼即使是最先進的算法, 該算法的複雜程度爲 O(n$^3$),其中 n 是樹中元素的數量
- 如果在
React
中使用了該算法, 那麼展示1000
個元素所需要執行的計算量將在十億
的量級範圍
- 如果在
-
這個開銷太過昂貴了, React 的更新性能會變得非常低效
-
於是,
React
對這個算法進行了優化,將其優化成了O(n)
,如何優化的呢?- 同層節點之間相互比較,不會跨節點比較
- 不同類型的節點,產生不同的樹結構
- 開發中,可以通過 key 來指定哪些節點在不同的渲染下保持穩定
情況一: 對比不同類型的元素
-
當節點爲不同的元素,React 會拆卸原有的樹,並且建立起新的樹:
- 當一個元素從
<a>
變成<img>
,從<Article>
變成<Comment>
,或從<button>
變成<div>
都會觸發一個完整的重建流程 - 當卸載一棵樹時,對應的
DOM
節點也會被銷燬,組件實例將執行componentWillUnmount()
方法 - 當建立一棵新的樹時,對應的
DOM
節點會被創建以及插入到DOM
中,組件實例將執行componentWillMount()
方法,緊接着componentDidMount()
方法
- 當一個元素從
-
比如下面的代碼更改:
- React 會銷燬 Counter 組件並且重新裝載一個新的組件,而不會對 Counter 進行復用
情況二: 對比同一類型的元素
-
當比對兩個相同類型的 React 元素時,React 會保留 DOM 節點,僅對比更新有改變的屬性
-
比如下面的代碼更改:
- 通過比對這兩個元素,
React
知道只需要修改DOM
元素上的className
屬性
- 通過比對這兩個元素,
-
比如下面的代碼更改:
- 當更新
style
屬性時,React
僅更新有所改變的屬性。 - 通過比對這兩個元素,
React
知道只需要修改DOM
元素上的color
樣式,無需修改fontWeight
- 當更新
-
如果是同類型的組件元素:
- 組件會保持不變,
React
會更新該組件的props
,並且調用componentWillReceiveProps()
和componentWillUpdate()
方法 - 下一步,調用
render()
方法,diff
算法將在之前的結果以及新的結果中進行遞歸
- 組件會保持不變,
情況三: 對子節點進行遞歸
-
在默認條件下,當遞歸
DOM
節點的子元素時,React
會同時遍歷兩個子元素的列表;當產生差異時,生成一個mutation
-
我們來看一下在最後插入一條數據的情況:👇
-
前面兩個比較是完全相同的,所以不會產生 mutation
-
最後一個比較,產生一個 mutation,將其插入到新的 DOM 樹中即可
-
-
但是如果我們是在前面插入一條數據:
- React 會對每一個子元素產生一個 mutation,而不是保持
<li>星際穿越</li>
和<li>盜夢空間</li>
的不變 - 這種低效的比較方式會帶來一定的性能問題
- React 會對每一個子元素產生一個 mutation,而不是保持
React 性能優化
1.key 的優化
- 我們在前面遍歷列表時,總是會提示一個警告,讓我們加入一個
key
屬性:
-
方式一:在 最後 位置插入數據
- 這種情況,有無
key
意義並不大
- 這種情況,有無
-
方式二:在 前面 插入數據
- 這種做法,在沒有
key
的情況下,所有的<li>
都需要進行修改
- 這種做法,在沒有
-
在下面案例: 當子元素 (這裏的
li
元素) 擁有key
時React
使用key
來匹配原有樹上的子元素以及最新樹上的子元素:- 將
key
爲333
的元素插入到最前面的位置即可
key
的注意事項:
key
應該是唯一的key
不要使用隨機數(隨機數在下一次 render 時,會重新生成一個數字)- 使用
index
作爲key
,對性能是沒有優化的
2.render 函數被調用
-
我們使用之前的一個嵌套案例:
- 在 App 中,我們增加了一個計數器的代碼
-
當點擊
+1
時,會重新調用App
的render
函數- 而當 App 的 render 函數被調用時,所有的子組件的 render 函數都會被重新調用
-
那麼,我們可以思考一下,在以後的開發中,我們只要是修改 了 App 中的數據,所有的子組件都需要重新
render
,進行diff
算法,性能必然是很低的:- 事實上,很多的組件沒有必須要重新
render
- 它們調用 render 應該有一個前提,就是依賴的數據 (state、 props) 發生改變時,再調用自己的
render
方法
- 事實上,很多的組件沒有必須要重新
-
如何來控制
render
方法是否被調用呢?- 通過
shouldComponentUpdate
方法即可
- 通過
3.shouldComponentUpdate
React
給我們提供了一個生命週期方法shouldComponentUpdate
(很多時候,我們簡稱爲SCU
),這個方法接受參數,並且需要有返回值;主要作用是:**控制當前類組件對象是否調用render
**方法
-
該方法有兩個參數:
- 參數一:
nextProps
修改之後, 最新的porps
屬性 - 參數二:
nextState
修改之後, 最新的state
屬性
- 參數一:
-
該方法返回值是一個 booolan 類型
- 返回值爲
true
, 那麼就需要調用render
方法 - 返回值爲
false
, 那麼不需要調用render
方法
- 返回值爲
-
比如我們在 App 中增加一個
message
屬性:JSX
中並沒有依賴這個message
, 那麼它的改變不應該引起重新渲染- 但是通過
setState
修改state
中的值, 所以最後render
方法還是被重新調用了
// 決定當前類組件對象是否調用render方法
// 參數一: 最新的props
// 參數二: 最新的state
shouldComponentUpdate(nextProps, nextState) {
// 默認是: return true
// 不需要在頁面上渲染則不調用render函數
return false
}
4.PureComponent
-
如果所有的類, 我們都需要手動來實現
shouldComponentUpdate
, 那麼會給我們開發者增加非常多的工作量- 我們設想一下在
shouldComponentUpdate
中的各種判斷目的是什麼? props
或者state
中數據是否發生了改變, 來決定shouldComponentUpdate
返回true
或false
- 我們設想一下在
-
事實上
React
已經考慮到了這一點, 所以React
已經默認幫我們實現好了, 如何實現呢?- 將 class 繼承自 PureComponent
- 內部會進行淺層對比最新的
state
和porps
, 如果組件內沒有依賴porps
或state
將不會調用render
- 解決的問題: 比如某些子組件沒有依賴父組件的
state
或props
, 但卻調用了render
函數
5.shallowEqual 方法
這個方法中,調用
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
,這個shallowEqual
就是進行淺層比較:
6. 高階組件 memo
-
函數式組件如何解決
render
: 在沒有依賴state
或props
但卻重新渲染render
問題 -
我們需要使用一個高階組件
memo
:- 我們將之前的 Header、Banner、ProductList 都通過 memo 函數進行一層包裹
- Footer 沒有使用 memo 函數進行包裹;
- 最終的效果是,當
counter
發生改變時,Header、Banner、ProductList 的函數不會重新執行,而 Footer 的函數會被重新執行
import React, { PureComponent, memo } from 'react'
// MemoHeader: 沒有依賴props,不會被重新調用render渲染
const MemoHeader = memo(function Header() {
console.log('Header被調用')
return <h2>我是Header組件</h2>
})
React 知識點總結腦圖
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://segmentfault.com/a/1190000039776687