在hooks誕生以前,若是組件包含內部 state
,咱們都是基於 class
的形式來建立組件。css
在react中,性能優化的點在於:react
setState
,就會觸發組件的從新渲染,不管先後 state
是否相同基於上面的兩點,咱們一般的解決方案是:使用 immutable
進行比較,在不相等的時候調用 setState
, 在 shouldComponentUpdate
中判斷先後的 props
和 state
,若是沒有變化,則返回 false
來阻止更新。ios
在 hooks
出來以後,函數組件中沒有 shouldComponentUpdate
生命週期,咱們沒法經過判斷先後狀態來決定是否更新。useEffect
再也不區分 mount
update
兩個狀態,這意味着函數組件的每一次調用都會執行其內部的全部邏輯,那麼會帶來較大的性能損耗。c++
咱們先簡單的看一下useMemo和useCallback的調用簽名:typescript
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T; function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
複製代碼
useCallback
和 useMemo
的參數跟 useEffect
一致,他們之間最大的區別有是 useEffect
會用於處理反作用,而前兩個hooks不能。api
useCallback
和 useMemo
都會在組件第一次渲染的時候執行,以後會在其依賴的變量發生改變時再次執行;而且這兩個hooks都返回緩存的值,useMemo
返回緩存的 變量,useCallback
返回緩存的 函數數組
在 class 組件時代,爲了性能優化咱們常常會選擇使用 PureComponent
,每次對 props 進行一次淺比較,固然,除了 PureComponent 外,咱們還能夠在 shouldComponentUpdate
中進行更深層次的控制。緩存
在 Function 組件中, React 貼心的提供了 React.memo
這個 HOC(高階組件),與 PureComponent 很類似,可是是專門給 Function Component 提供的,對 Class Component 並不適用。性能優化
可是相比於 PureComponent ,React.memo()
能夠支持指定一個參數
,能夠至關於 shouldComponentUpdate
的做用,所以 React.memo() 相對於 PureComponent 來講,用法更加方便。網絡
(固然,若是本身封裝一個 HOC,而且內部實現 PureComponent + shouldComponentUpdate 的結合使用,確定也是 OK 的,在以往項目中,這樣使用的方式還挺多)
首先看下 React.memo() 的使用方式:
function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/* return true if passing nextProps to render would return the same result as passing prevProps to render, otherwise return false */
}
export default React.memo(MyComponent, areEqual);
複製代碼
使用方式很簡單,在 Function Component 以外,在聲明一個 areEqual
方法來判斷兩次 props
有什麼不一樣,若是第二個參數不傳遞,則默認只會進行 props 的淺比較
最終 export 的組件,就是 React.memo() 包裝以後的組件。
實例:
index.js:父組件
Child.js:子組件
ChildMemo.js:使用 React.memo 包裝過的子組件
import React, { useState, } from 'react';
import Child from './Child';
import ChildMemo from './Child-memo';
export default (props = {}) => {
const [step, setStep] = useState(0);
const [count, setCount] = useState(0);
const [number, setNumber] = useState(0);
const handleSetStep = () => {
setStep(step + 1);
}
const handleSetCount = () => {
setCount(count + 1);
}
const handleCalNumber = () => {
setNumber(count + step);
}
return (
<div>
<button onClick={handleSetStep}>step is : {step} </button>
<button onClick={handleSetCount}>count is : {count} </button>
<button onClick={handleCalNumber}>numberis : {number} </button>
<hr />
<Child step={step} count={count} number={number} /> <hr />
<ChildMemo step={step} count={count} number={number} />
</div>
);
}
複製代碼
這個子組件自己沒有任何邏輯,也沒有任何包裝,就是渲染了父組件傳遞過來的 props.number
須要注意的是,子組件中並無使用到 props.step
和 props.count
,可是一旦 props.step
發生了變化就會觸發從新渲染。
import React from 'react';
export default (props = {}) => {
console.log(`--- re-render ---`);
return (
<div> {/* <p>step is : {props.step}</p> */} {/* <p>count is : {props.count}</p> */} <p>number is : {props.number}</p> </div>
);
};
複製代碼
這個子組件使用了 React.memo 進行了包裝,而且經過 isEqual
方法判斷只有當兩次 props 的 number
的時候纔會從新觸發渲染,不然 console.log
也不會執行。
import React, { memo, } from 'react';
const isEqual = (prevProps, nextProps) => {
if (prevProps.number !== nextProps.number) {
return false;
}
return true;
}
export default memo((props = {}) => {
console.log(`--- memo re-render ---`);
return (
<div> {/* <p>step is : {props.step}</p> */} {/* <p>count is : {props.count}</p> */} <p>number is : {props.number}</p> </div>
);
}, isEqual);
複製代碼
經過上圖能夠發現,在點擊 step 和 count 的時候,props.step 和 props.count 都發生了變化,所以 Child.js
這個子組件每次都在從新執行渲染(----re-render----
),即便沒有用到這兩個 props。
而這種狀況下,ChildMemo.js
則不會從新進行 re-render。
只有當 props.number 發生變化的時候,ChildMemo.js
和 Child.js
表現是一致的。
從上面能夠看出,React.memo() 的第二個方法在某種特定需求下,是必須存在的。 由於在實驗的場景中,咱們可以看得出來,即便我使用 React.memo
包裝了 Child.js,也會一直觸發從新渲染,由於 props 淺比較確定是發生了變化。
上面 React.memo() 的使用咱們能夠發現,最終都是在最外層包裝了整個組件,而且須要手動寫一個方法比較那些具體的 props 不相同才進行 re-render。
而在某些場景下,咱們只是但願 component 的部分不要進行 re-render,而不是整個 component 不要 re-render,也就是要實現 局部 Pure
功能。
useMemo()
基本用法以下:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼
useMemo() 返回的是一個 memoized 值,只有當依賴項(好比上面的 a,b 發生變化的時候,纔會從新計算這個 memoized 值)
memoized 值不變的狀況下,不會從新觸發渲染邏輯。
提及渲染邏輯,須要記住的是 useMemo() 是在 render 期間執行的,因此不能進行一些額外的副操做,好比網絡請求等。
若是沒有提供依賴數組(上面的 [a,b])則每次都會從新計算 memoized 值,也就會 re-redner
上面的代碼中新增一個 Child-useMemo.js
子組件以下:
import React, { useMemo } from 'react';
export default (props = {}) => {
console.log(`--- component re-render ---`);
return useMemo(() => {
console.log(`--- useMemo re-render ---`);
return <div> {/* <p>step is : {props.step}</p> */} {/* <p>count is : {props.count}</p> */} <p>number is : {props.number}</p> </div>
}, [props.number]);
}
複製代碼
與上面惟一的區別是使用的 useMemo() 包裝的是 return 部分渲染的邏輯,而且聲明依賴了 props.number,其餘的並未發生變化。
效果對比:
上面圖中咱們能夠發現,父組件每次更新 step/count 都會觸發 useMemo 封裝的子組件的 re-render,可是 number 沒有變化,說明並無從新觸發 HTML 部分 re-render
只有當依賴的 props.number 發生變化的時候,纔會從新觸發 useMemo() 包裝的裏面的 re-render
講完了useMemo,接下來是 useCallback
。useCallback 跟 useMemo 比較相似,但它返回的是緩存的函數。咱們看一下最簡單的用法:
const fnA = useCallback(fnB, [a])
複製代碼
實例:
import React, { useState, useCallback } from 'react';
import Button from './Button';
export default function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleClickButton1 = () => {
setCount1(count1 + 1);
};
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, [count2]);
return (
<div> <div> <Button onClickButton={handleClickButton1}>Button1</Button> </div> <div> <Button onClickButton={handleClickButton2}>Button2</Button> </div> </div>
);
}
複製代碼
Button組件
// Button.jsx
import React from 'react';
const Button = ({ onClickButton, children }) => {
return (
<> <button onClick={onClickButton}>{children}</button> <span>{Math.random()}</span> </> ); }; export default React.memo(Button); 複製代碼
這裏或許會注意到
React.memo
這個方法,此方法內會對props
作一個淺層比較,若是若是props
沒有發生改變,則不會從新渲染此組件。
上面的 Button
組件都須要一個 onClickButton 的 props ,儘管組件內部有用 React.memo
來作優化,可是咱們聲明的 handleClickButton1
是直接定義了一個方法,這也就致使只要是父組件從新渲染(狀態或者props更新)就會致使這裏聲明出一個新的方法,新的方法和舊的方法儘管長的同樣,可是依舊是兩個不一樣的對象,React.memo
對比後發現對象 props 改變,就從新渲染了。
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, [count2]);
複製代碼
上述代碼咱們的方法使用 useCallback 包裝了一層,而且後面還傳入了一個 [count2]
變量,這裏 useCallback 就會根據 count2 是否發生變化,從而決定是否返回一個新的函數,函數內部做用域也隨之更新。
因爲咱們的這個方法只依賴了 count2 這個變量,並且 count2 只在點擊 Button2 後纔會更新 handleClickButton2
,因此就致使了咱們點擊 Button1 不從新渲染 Button2 的內容。
在子組件不須要父組件的值和函數的狀況下,只須要使用 memo
函數包裹子組件便可。
若是有函數傳遞給子組件,使用 useCallback
若是有值傳遞給子組件,使用 useMemo
useEffect
、useMemo
、useCallback
都是自帶閉包的。也就是說,每一次組件的渲染,其都會捕獲當前組件函數上下文中的狀態(state
, props
),因此每一次這三種hooks的執行,反映的也都是當前的狀態,你沒法使用它們來捕獲上一次的狀態。對於這種狀況,咱們應該使用 ref
來訪問。