16.6以前,函數組件沒有像
shouldComponentUpdate
這樣的方法,也沒有相似PureComponent
這種解決方案,避免不了函數組件裏面全部的代碼再次的執行,要依靠外面的條件渲染來控制,或者是高階組件。以前的話,選擇使用函數組件的狀況是一些比較簡單的又比較純的組件,只是負責展現的。並且函數組件最終編譯babel結果是隻執行createElement
那一步;class組件同樣有生命週期要實例化,最終通過Babel成es5代碼的時候還很長前端
當16.6的memo問世,函數組件就有了相似 PureComponent
和 shouldComponentUpdate
的解決方案,memo的使用方法:react
const C = (props) => {
數組
return <section>{props.name}你太美</section>
性能優化
}
babel
export default React.memo(C)
app
當父組件執行render的時候,避免不了C組件的渲染和C函數的執行(若是不在外面加判斷的話:{isShowC&&<C/>}
)。當到了C組件的時候,會淺比較C組件先後props值。若是props每個屬性值都同樣,會跳過函數組件C的執行,減小了沒必要要的渲染,達到了性能優化。ide
第二個參數,是一個函數,該函數傳入參數是新props和上次props,咱們能夠在函數裏面作判斷邏輯,控制返回值。當咱們讓函數return true的時候,告訴了react這兩個props是同樣的,不用從新執行整個函數組件;反之false的時候會從新執行該組件函數
memo(IfEqual, () => false);
好比這行代碼,判斷函數一直返回false, memo
包住的 IfEqual
組件不管怎樣都會從新執行性能
當咱們用上了memo,就能夠根據業務來進行優化了:優化
React.memo(C, (nextProps, prevProps) => { // 作咱們想作的事情,相似shouldComponentUpdate})
咱們都知道,js中函數不是簡單數據類型,也就是說 function(){}
和 function(){}
是不同的,與 {}
和 {}
不同同理。那麼咱們傳入 props.onClick
(即便是長得同樣的內容徹底同樣),先後 props.onClick
都不能劃上等號
<div> <IfEqual onClick={() => {}} /> </div>
以爲inline function很差看,那前面定義一下,實際上仍是逃不了同一個事情:它們是不同的。此次是由於,函數組件的渲染,也就是執行,每一次從新執行,函數做用域裏面一切都是從新開始。這就至關於上一次組件渲染 consthandleClick=()=>{}
,後面渲染又一次 consthandleClick=()=>{}
,它們都不是同一個東西
export default () => { const handleClick = () => {} return ( <div> <IfEqual onClick={handleClick} /> </div> )}
這種狀況下,咱們能夠用memo第二個參數來拯救多餘一次的渲染的局面:
// props: { a: 1, onClick: () => {} }// 在咱們知道onClick是作同一個事情的函數的前提下,不比較onClickReact.memo(C, (nextProps, prevProps) => nextProps.a === prevProps.a)
最後,先後props的 onClick
,它們只有一種狀況是同樣的——把聲明抽到組件外面去
const handleClick = () => {} export default () => { return ( <div> <IfEqual onClick={handleClick} /> </div> )}
這時候,有沒有想起class組件裏面老是 onClick={this.handleClick}
呢?this.handleClick
一直都是同一個函數。這種狀況,子組件爲函數組件的時候,包一層memo就能夠實現purecomponent的效果
函數組件把函數定義寫在外面,是能夠解決問題。可是,若是handleClick依賴組件內部的一些變量,那handleClick又不得不寫在裏面(固然利用引用類型能夠解決)。或者仍是正常寫,靠memo第二個參數來控制要不要從新渲染子函數組件。可是不管怎樣,都存在一個問題,就是那一塊代碼寫在裏面呢,都沒法避免代碼的執行和函數的從新定義,好比
function a(){ const b = function(){ console.log(1) // 不少不少代碼 }}a()a() // 函數b又被定義了一次
若是咱們經過依賴來肯定先後兩次是否是同一個函數,咱們能夠用函數記憶來實現整個功能
// 半僞代碼
let prev
let prevDeps
function memorize(fn, deps) {
// 先後依賴一致,不用從新計算直接返回上次結果
if (prev && isEqual(deps, prevDeps)) {
return prev
}
prevDeps = deps
prev = fn
return fn
}
function a(){
const b = memorize(function(){
console.log(1)
// 不少不少代碼
}, [])
}
a()
a() // 函數b是同一個
相似函數記憶的原理,後來有了 useCallback
的出現,多了一種新的解決方案,根據依賴生成一個函數:
const handleClick = useCallback(() => { console.log(dep)}, [dep])
當dep不變,每一次函數組件的執行,handleClick都是同一個函數。若是dep變了,那麼handleClick又是一個新的函數
export default () => {
// 沒有依賴,永遠是同一個函數
const handleClick = useCallback(() => {}, []);
// 依賴a,從新執行函數組件,a不變的,是同一個函數
// a變了handleClick是新的函數
const handleClick1 = useCallback(() => {}, [a]);
return (
<div>
<IfEqual onClick={handleClick} />
</div>
)
}
react組件也是一個函數,那其實 useCallback
還能夠作一個函數組件:
export default () => {
const handleClick = useCallback(() => {}, []);
const Cpn = useCallback(({ name }) => {
return <button onClick={handleClick}>{name}</button>
}, [handleClick]);
return (
<div>
<Cpn name="hi" />
</div>
)
}
固然這只是一個簡單的場景,若是用了hooks,尚未解決問題或者暫時沒有想到優雅的封裝技巧,想用高階組件的時候,不妨嘗試一下 useCallback
const a = useMemo(() => memorizeValue, deps)
當deps不變,a的值仍是上次的memorizeValue,省去了從新計算的過程。若是memorizeValue是一個函數,和useCallback是同樣的效果:
useCallback(fn, inputs) <=> useMemo(() => fn, inputs)
咱們能夠試一下同步執行的代碼,當時間很是長的時候,useMemo能夠發揮它的做用了:
// 強行更新組件
const useForceUpdate = () => {
const forceUpdate = useState(0)[1];
return () => forceUpdate(x => x + 1);
}
// 一個很耗時間的代碼
function slowlyAdd(n) {
console.time('add slowly')
let res = n;
for (let i = 0; i < 2000000000; i++) {
res += 1;
}
console.timeEnd('add slowly')
return res;
}
// useMemo記憶結果的一個自定義hook
function useSlowlyAdd(n) {
const res = useMemo(() => {
return slowlyAdd(n);
}, [n])
return res;
}
export default () => {
const [count, add] = useState(1);
const forceUpdate = useForceUpdate();
const handleClick = useCallback(() => {}, []);
useSlowlyAdd(count) // 第一次這裏會耗不少時間,頁面卡死一陣
return (
<>
<button onClick={forceUpdate}>更新頁面</button>
<button onClick={() => add(count + 1)}>+</button>
</>
)
}
第一次進來,頁面暫時沒有任何反應一陣,這是由於slowlyAdd佔用了js主線程。當咱們點擊‘更新頁面’更新的時候,頁面並無卡死,並且組件也從新渲染執行了一次。當咱們點擊+,頁面又開始卡死一陣。
這是由於點擊+的時候,修改了useMemo的依賴n,n變了從新計算,計算耗費時間。若是點擊更新頁面,沒有修改到依賴n,不會從新計算,頁面也不會卡
固然,useMemo也能夠作高階組件,用起來的時候,能夠寫成reactElement的形式了:
const HOC = useMemo(() => <C />, deps)
有以下的組件,Big是一個10w個節點的組件,每個節點都綁定事件
const handleClick = useCallback(() => {}, []);export default () => { return ( <div> <Big onClick={handleClick} /> </div> )}
若是Big組件沒有memo包住,首次掛載和再次渲染父組件性能以下:
若是Big組件有memo包住並且props被認爲是同樣的狀況下,首次掛載和再次渲染父組件性能以下:
可是性能優化不是免費午飯,不是全部的函數組件都包memo,組件裏面的函數都包usecallback就行了。由於具備memorize,沒有優化的意義的狀況下強行優化,反而是性能惡化。
總結一下對於props的某個屬性值爲函數的時候,如何作到子組件不從新執行多餘渲染:
IMWeb 團隊隸屬騰訊公司,是國內最專業的前端團隊之一。
咱們專一前端領域多年,負責過 QQ 資料、QQ 註冊、QQ 羣等億級業務。目前聚焦於在線教育領域,精心打磨 騰訊課堂、企鵝輔導 及 ABCMouse 三大產品。