簡介
正常來講的話當我們點擊組件的時候,該組件以及該組件的子組件都會重新渲染,但是如何避免子組件重新渲染呢,我們經常用memo來解決
React.memo配合useCallback緩存組件
- 父組件沒有傳props
const Index = ()=> {console.log('子組件刷新了');return (<div>這是子組件</div>)
}
//這里我們用react.memo對組件進行包裹,包裹一次之后react在render的過程中不會給該fiber打上更新的tag
//從而跳過更新,這個原理其實就是react.memo的第二個參數上,如果react.memo第二個參數不傳遞,react回默
//認給我們補充上第二個參數的邏輯,其中邏輯就是淺比較Index組件的props參數,如果相等的話默認第二個參數返
//回true,組件就會緩存了,如果不相等的話就會返回false組件就會重新打上更新的tag然后重新渲染。
const MemoIndex = React.memo(Index);const App = ()=>{const [state, setState] = useState(0);return (<div className="App"><button onClick={()=>setState(state+1)}>點我看看子組件刷新了嗎</button><MemoIndex/></div>);
}
- 父組件傳了state
const Index = ()=> {console.log('子組件刷新了');return (<div>這是子組件</div>)
}
//其實這Index組件不會更新的,react檢測到我們不傳遞第二個參數的話,會把之前的props拆出來,和現在的
//props做比較 發現pre.next === cur.next 然后回返回true組件就會緩存了
const MemoIndex = React.memo(Index);
//這行代碼就相當這樣的代碼
const MemoIndex = React.memo(Index, (pre, cur)=> {//這樣寫的就比較簡單了,因為這是是針對于當前的demo來說的。react比較的代碼邏輯比較復雜,因為react//需要考慮到多種情況,props中參數可能多一個少一個的情況,所以react默認提供的代碼比較復雜if(pre.name === cur.name) {return true;}return false;
});const App = ()=>{const [state, setState] = useState(0);return (<div className="App"><button onClick={()=>setState(state+1)}>點我看看子組件刷新了嗎</button><MemoIndex name={0}/></div>);
}
- 父組件傳了函數
const Index = ()=> {console.log('子組件刷新了');return (<div>這是子組件</div>)
}
const MemoIndex = React.memo(Index);const App = ()=>{const [state, setState] = useState(0);const func = ()=> {};return (<div className="App">//這時候子組件會不會刷新呢,有的同學可能說不會因為淺比較發現pre.func === cur.func 返回//true所以不會刷新,但是其實是會刷新的,因為APP組件中觸發了setState之后App組件重新渲染,也//就是相當于執行了App()這個方法,所以里面func的指向地址發生了變化,所以pre.func !== //cur.func 子組件會重新渲染,<button onClick={()=>setState(state+1)}>點我看看子組件刷新了嗎</button><MemoIndex func={func}/></div>);
}
使用useCallback緩存函數
const Index = ()=> {console.log('子組件刷新了');return (<div>這是子組件</div>)
}const MemoIndex = React.memo(Index);const App = ()=>{const [state, setState] = useState(0);//這里我們用了useCallback,useCallback主要是緩存我們當前的函數,如果我們第二個參數傳遞空數組的話//他的地址不會改變,如果我們第二個參數傳遞的是一個變量,這個變量發生變化他的地址就會發生變化。所以這//和useEffect的第二個參數是一樣的,但是請注意不要濫用useCallback的第二個參數。如果第二個參數濫用//會拿到我們之前的值。我們看下一個示例就知道了const func = useCallback(()=> {}, [])return (<div className="App"><button onClick={()=>setState(state+1)}>點我看看子組件刷新了嗎</button><MemoIndex func={func}/></div>);
}
使用useMemo緩存組件
useMemo不僅可以緩存變量,函數還可以緩存組件
const Index = (props)=> {console.log('子組件刷新了');return (<div>這是子組件</div>)
}const App = ()=>{const [state, setState] = useState(0);//使用useMemo也要和useCallback一樣特別注意第二個參數,因為他有可能導致我們拿不到最新的數據解決//解決方案就和useCallback的一樣,簡單來說套用react官方的話就是請確保數組中包含了所有外部作用域//中會隨時間變化并且在useMemo中使用的變量都要放到第二個參數中。const Component = useMemo(()=><Index/>, []);return (<div className="App"><button onClick={()=>setState(state+1)}>點我看看子組件刷新了嗎</button>{Component}</div>);
}
利用props.children緩存組件
這樣在Index組件re-render的時候,由于App(父組件)中的組件沒有變化,所以拿到的children依然是上一次的(沒有發生變化的)所以children部分不會re-render。
const Index = (props)=> {const [state, setState] = useState(0);return (<div>//當這個按鈕點擊之后我們發現Children組件并不重新刷新了,其實原理我理解的是react幫我們做了一層//處理當渲染前與渲染后兩個組件的引用地址一樣他就會放棄render,當然這是我的猜測,這個的話之后//我看到源碼的時候會和大家講一下在補充一下。<button onClick={()=>setState(state+1)}>點我我看看子組件刷新不刷新</button>{props.children}</div>)
};
const Children = ()=> {return (<div>{console.log('子組件刷新了')}這是children組件</div>)
}const App = ()=>{return (<div className="App"><Index><Children/></Index></div>);
}
注意事項
- useCallBack不是每個函數都需要使用!不要濫用useCallback
const Index = (props)=> {console.log('子組件刷新了');return (<div>//點擊這個按鈕之前先點擊App組件下面的按鈕,讓state變大,然后在點擊這個按鈕看看state是啥//我們發現state一直是一個0。這是為什么呢,因為很簡單我們之前講了useCallback第二個和//useEffect的第二個參數是一樣的,因為我們傳遞的是空數組說以useCallback一直拿到的是最原始的//值,所以會造成這個問題,我們寫代碼的時候千萬要注意第二個參數,只要useCallback需要什//值我們就在第二個參數傳遞什么值,這樣才可以確保我們拿到的是最新的值。同樣的里面如果不需要一些//參數的話我們也不要把這些參數加到第二個參數上面否則會出現func的地址多次改變。<button onClick={props.func}>點我看看state是啥</button>這是子組件</div>)
}
const MemoIndex = React.memo(Index);const App = ()=>{const [state, setState] = useState(0);const func = useCallback(()=> {console.log(state);}, [])return (<div className="App"><button onClick={()=>setState(state+1)}>點我看看子組件刷新了嗎</button><MemoIndex func={func}/></div>);
}
- useCallBack是一個緩存工具沒錯。但實際上他并不能阻止函數都重現構建。
示例:
大家看上方這種結構的組件,Com組件中包含了fun1和fun2兩個函數。
是不是認為當Com組件重新渲染的時候,只有fun2(沒有使用useCallBack的函數)函數會被重新構建,而fun1(使用了useCallBack的函數)函數不會被重新構建。
實際上,被useCallBack包裹了的函數也會被重新構建并當成useCallBack函數的實參傳入。
useCallBack的本質工作不是在依賴不變的情況下阻止函數創建,而是在依賴不變的情況下不返回新的函數地址而返回舊的函數地址。不論是否使用useCallBack都無法阻止組件render時函數的重新創建!!
每一個被useCallBack的函數都將被加入useCallBack內部的管理隊列。而當我們大量使用useCallBack的時候,管理隊列中的函數會非常之多,任何一個使用了useCallBack的組件重新渲染的時候都需要去遍歷useCallBack內部所有被管理的函數找到需要校驗依賴是否改變的函數并進行校驗。
在以上這個過程中,尋找指定函數需要性能,校驗也需要性能。所以,濫用useCallBack不但不能阻止函數重新構建還會增加“尋找指定函數和校驗依賴是否改變”這兩個功能,為項目增添不必要的負擔。
//Com組件
const Com = () => {//示例1包裹了useCallBack的函數const fun1 = useCallBack(() => {console.log('示例一函數');...},[])//示例2沒有包裹useCallBack的函數const fun2 = () => {console.log('示例二函數');...}return <div></div>
}
不要過度緩存組件
其實不必過度優化代碼 react官方沒有幫你做 其實也證明了 如果你的代碼沒有明顯的卡頓 你自己去做優化 可能造成負優化
優化的手段一般都是針對組件本身比較復雜且數據量大每次re-render都會造成卡頓的情況下才去做的,沒有太明顯的卡頓出現時沒必要做這些優化。而且在使用memo前其實也有手段去規避這些無效的re-render,比如將組件粒度劃分的更細一些
參考文章
useCallBack你真的知道怎么用嗎。