前言
本文主要分享一下 React Hooks 性能優化可以從哪幾個方面入手
Hooks的性能問題
要想解決性能問題,關鍵在於組件重復渲染的處理。在使用 React Hooks
后,很多人會抱怨渲染次數變多,比如我們會把不同的數據分成多個 state
變量,每個值的變化都會觸發一次渲染。
舉個🌰
現在有個父子組件
,子組件依賴父組件傳入的 name
屬性,但是父組件 name
屬性和 text
屬性變化都會導致 Parent
函數重新執行,所以即使傳入子組件 props
沒有任何變化,甚至子組件沒有依賴於任何 props
屬性,都會導致子組件重新渲染
const Child = ((props: any) => {
console.log("公眾號:前端開發愛好者 的子組件,我更新了...");
return (
<div>
<h3>子組件</h3>
<div>text:{props.name}</div>
<div>{new Date().getTime()}</div>
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("")
const handleClick = () => {
setCount(count + 1);
}
const handleInputChange = (e) => {
setText(e.target.value)
}
return (<div>
<input onChange={handleInputChange} />
<button onClick={handleClick}>+1</button>
<div>count:{count}</div>
<Child name ={text}/>
</div>)
}
上面的代碼執行你會發現,不管是觸發 handleInputChange
還是觸發 handleClick
,子組件都會在控制台輸出 我是前端開發愛好者的子組件,我更新了... 所以即使傳入子組件 props 沒有任何變化
,甚至子組件沒有依賴於任何 props 屬性
,子組件都會重新渲染
。
想要解決重復渲染的問題,可以使用 react 的親手制造升級的兒子,他有三個方法用來做優化,分別是 React.memo useCallback useMemo
。
-
React.memo : 和 class 組件時期的 PureComponent 一樣,做簡單額數據類型比較
-
useMemo : 可以用來比較復雜類型的數據,不如 Object Array 等
-
useCallback : 升級版本,用於控制傳遞函數時候控制組件是否需要更新
React.memo
使用 memo 包裹子組件時,只有 props 發生改變子組件才會重新渲染。使用 memo 可以提升一定的性能。
const Child = React.memo((props: any) => {
console.log("公眾號:前端開發愛好者 的子組件,我更新了..."); // 只有當props屬性改變,集name屬性改變時,子組件才會重新渲染
return (
<div>
<h3>子組件</h3>
<div>text:{props.name}</div>
<div>{new Date().getTime()}</div>
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("")
const handleClick = () => {
setCount(count + 1);
}
const handleInputChange = (e) => {
setText(e.target.value)
}
return (<div>
<input onChange={handleInputChange} />
<button onClick={handleClick}>+1</button>
<div>count:{count}</div>
<Child name ={text}/>
</div>)
}
但如果傳入的 props
包含函數
,父組件每次重新渲染都是創建新的函數,所以傳遞函數子組件還是會重新渲染
,即使函數的內容還是一樣。如何解決這一問題,我們希望把函數也緩存起來,於是引入 useCallback
useCallback
useCallback
用於緩存函數,只有當依賴項改變時,函數才會重新執行返回新的函數,對於父組件中的函數作為 props 傳遞給子組件時,只要父組件數據改變,函數重新執行,作為 props 的函數也會產生新的實例,導致子組件的刷新
使用
useCallback
可以緩存函數。需要搭配memo
使用
const Child = React.memo((props: any) => {
console.log("公眾號:前端開發愛好者 的子組件,我更新了...");
return (
<div>
<h3>子組件</h3>
<div>text:{props.name}</div>
<div> <input onChange={props.handleInputChange} /></div>
<div>{new Date().getTime()}</div>
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("")
const handleClick = () => {
setCount(count + 1);
}
const handleInputChange = useCallback((e) => {
setText(e.target.value )
},[])
return (<div>
<button onClick={handleClick}>+1</button>
<div>count:{count}</div>
<Child name={text} handleInputChange={handleInputChange}/>
</div>)
}
useCallback
第二個參數依賴項什么情況下使用呢,看下面的例子
//修改handleInputChange
const handleInputChange =useCallback((e) => {
setText(e.target.value + count)
},[])
以上例子 count
改變會發生什么事呢?
count
改變,但handleInputChange
不依賴與任何項,所以handleInputChange
只在初始化的時候調用一次函數就被緩存起來,當文本改變時或者count
改變時函數內部的count
始終為0
,至於為什么需要看useCallback
源碼后解答。
所以需要將
count
加入到依賴項,count
變化后重新生成新的函數,改變函數內部的count
值
const handleInputChange =useCallback((e) => {
setText(e.target.value + count)
},[count])
useMemo
useMemo
使用場景請看下面這個例子,getTotal
假設是個很昂貴的操作,但該函數結果僅依賴於count
和price
,但是由於color
變化,DOM 重新渲染也會導致該函數的執行,useMemo
便是用於緩存該函數的執行結果,僅當依賴項改變后才會重新計算
const Parent = () => {
const [count, setCount] = useState(0);
const [color,setColor] = useState("");
const [price,setPrice] = useState(10);
const handleClick = () => {
setCount(count + 1);
}
const getTotal = ()=>{
console.log("getTotal 執行了 ...") // 該函數依賴於count和price,但color變化也會導致該函數的執行
return count * price
}
return (<div>
<div> 顏色: <input onChange={(e) => setColor(e.target.value)}/></div>
<div> 單價: <input value={price} onChange={(e) => setPrice(Number(e.target.value))}/></div>
<div> 數量:{count} <button onClick={handleClick}>+1</button></div>
<div>總價:{getTotal()}</div>
</div>)
}
修改后如下,注意 useMemo
緩存的是函數執行的結果
const Parent = () => {
console.log("parent 執行了...")
const [count, setCount] = useState(0);
const [color,setColor] = useState("");
const [price,setPrice] = useState(10);
const handleClick = () => {
setCount(count + 1);
}
const getTotal = useMemo(()=>{
console.log("getTotal 執行了 ...")
return count * price
},[count, price])
return (<div>
<div> 顏色: <input onChange={(e) => setColor(e.target.value)}/></div>
<div> 單價: <input value={price} onChange={(e) => setPrice(Number(e.target.value))}/></div>
<div> 數量:{count} <button onClick={handleClick}>+1</button></div>
<div>總價:{getTotal}</div>
</div>)
}
至此重復渲染的問題的解決基本上可以告一段落。
在 React 中是極力推薦函數式編程,可以讓數據不可變性作為我們優化的手段。我在 React class 時代大量使用了 immutable.js
結合 redux
來搭建業務,與 React 中 PureComponnet
完美配合,性能保持非常好。但是在 React hooks
中再結合 typescript
它就顯得有點格格不入了,類型支持得不是很完美。這里可以嘗試一下 immer.js
,引入成本小,寫法也簡潔了不少。