React Hooks 性能優(yōu)化,帶你玩轉(zhuǎn) Hooks
前言
React Hooks 出來(lái)很長(zhǎng)一段時(shí)間了,相信有不少朋友已經(jīng)深度使用了。無(wú)論是react本身還是其生態(tài)中,都在摸索著進(jìn)步。鑒于我使用react的經(jīng)驗(yàn),給大家分享一下 React Hooks性能優(yōu)化可以從哪幾個(gè)方面入手。至于React Hooks 的使用方式,本文就不做過(guò)多的講解了,可以自行查看https://react.docschina.org/docs/hooks-intro.html
問(wèn)題關(guān)鍵
要想解決性能問(wèn)題,關(guān)鍵在于組件重復(fù)渲染的處理。在使用 React Hooks 后,很多人會(huì)抱怨渲染次數(shù)變多,比如我們會(huì)把不同的數(shù)據(jù)分成多個(gè) state 變量,每個(gè)值的變化都會(huì)觸發(fā)一次渲染。
舉個(gè)例子: 現(xiàn)在有個(gè)父子組件,子組件依賴父組件傳入的name屬性,但是父組件name屬性和text屬性變化都會(huì)導(dǎo)致Parent函數(shù)重新執(zhí)行,所以即使傳入子組件props沒(méi)有任何變化,甚至子組件沒(méi)有依賴于任何props屬性,都會(huì)導(dǎo)致子組件重新渲染
const Child = ((props: any) => {console.log("我是前端開(kāi)發(fā)愛(ài)好者的子組件,我更新了...");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>)}
上面的代碼執(zhí)行你會(huì)發(fā)現(xiàn),不管是觸發(fā) handleInputChange 還是觸發(fā) handleClick ,子組件都會(huì)在控制臺(tái)輸出 我是前端開(kāi)發(fā)愛(ài)好者的子組件,我更新了... 所以即使傳入子組件props沒(méi)有任何變化,甚至子組件沒(méi)有依賴于任何props屬性,子組件都會(huì)重新渲染。
想要解決重復(fù)渲染的問(wèn)題,可以使用 react的親手制造升級(jí)的兒子,他有三個(gè)方法用來(lái)做優(yōu)化,分別是 React.memo useCallback useMemo 。
React.memo : 和 class 組件時(shí)期的 PureComponent 一樣,做簡(jiǎn)單額數(shù)據(jù)類型比較
useMemo : 可以用來(lái)比較復(fù)雜類型的數(shù)據(jù),不如 Object Array 等
useCallback : 升級(jí)版本,用于控制傳遞函數(shù)時(shí)候控制組件是否需要更新
React.memo
使用memo包裹子組件時(shí),只有props發(fā)生改變子組件才會(huì)重新渲染。使用memo可以提升一定的性能。
const Child = React.memo((props: any) => {console.log("我是前端開(kāi)發(fā)愛(ài)好者的子組件,我更新了..."); // 只有當(dāng)props屬性改變,集name屬性改變時(shí),子組件才會(huì)重新渲染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包含函數(shù),父組件每次重新渲染都是創(chuàng)建新的函數(shù),所以傳遞函數(shù)子組件還是會(huì)重新渲染,即使函數(shù)的內(nèi)容還是一樣。如何解決這一問(wèn)題,我們希望把函數(shù)也緩存起來(lái),于是引入useCallback
useCallback
useCallback用用于緩存函數(shù),只有當(dāng)依賴項(xiàng)改變時(shí),函數(shù)才會(huì)重新執(zhí)行返回新的函數(shù),對(duì)于父組件中的函數(shù)作為props傳遞給子組件時(shí),只要父組件數(shù)據(jù)改變,函數(shù)重新執(zhí)行,作為props的函數(shù)也會(huì)產(chǎn)生新的實(shí)例,導(dǎo)致子組件的刷新
使用useCallback可以緩存函數(shù)。需要搭配memo使用
const Child = React.memo((props: any) => {console.log("我是前端開(kāi)發(fā)愛(ài)好者的子組件,我更新了...");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第二個(gè)參數(shù)依賴項(xiàng)什么情況下使用呢,看下面的例子
//修改handleInputChangeconst handleInputChange =useCallback((e) => {setText(e.target.value + count)},[])
以上例子count改變會(huì)發(fā)生什么事呢?
count改變,但handleInputChange不依賴與任何項(xiàng),所以handleInputChange只在初始化的時(shí)候調(diào)用一次函數(shù)就被緩存起來(lái),當(dāng)文本改變時(shí)或者count改變時(shí)函數(shù)內(nèi)部的count始終為0,至于為什么需要看useCallback源碼后解答。
所以需要將count加入到依賴項(xiàng),count變化后重新生成新的函數(shù),改變函數(shù)內(nèi)部的count值
const handleInputChange =useCallback((e) => {setText(e.target.value + count)},[count])
useMemo
useMemo使用場(chǎng)景請(qǐng)看下面這個(gè)例子,getTotal假設(shè)是個(gè)很昂貴的操作,但該函數(shù)結(jié)果僅依賴于count和price,但是由于color變化,DOM重新渲染也會(huì)導(dǎo)致該函數(shù)的執(zhí)行,useMemo便是用于緩存該函數(shù)的執(zhí)行結(jié)果,僅當(dāng)依賴項(xiàng)改變后才會(huì)重新計(jì)算
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 執(zhí)行了 ...") // 該函數(shù)依賴于count和price,但color變化也會(huì)導(dǎo)致該函數(shù)的執(zhí)行return count * price}return (<div><div> 顏色: <input onChange={(e) => setColor(e.target.value)}/></div><div> 單價(jià): <input value={price} onChange={(e) => setPrice(Number(e.target.value))}/></div><div> 數(shù)量:{count} <button onClick={handleClick}>+1</button></div><div>總價(jià):{getTotal()}</div></div>)}
修改后如下,注意useMemo緩存的是函數(shù)執(zhí)行的結(jié)果
const Parent = () => {console.log("parent 執(zhí)行了...")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 執(zhí)行了 ...")return count * price},[count, price])return (<div><div> 顏色: <input onChange={(e) => setColor(e.target.value)}/></div><div> 單價(jià): <input value={price} onChange={(e) => setPrice(Number(e.target.value))}/></div><div> 數(shù)量:{count} <button onClick={handleClick}>+1</button></div><div>總價(jià):{getTotal}</div></div>)}
至此重復(fù)渲染的問(wèn)題的解決基本上可以告一段落。
在 React 中是極力推薦函數(shù)式編程,可以讓數(shù)據(jù)不可變性作為我們優(yōu)化的手段。我在 React class 時(shí)代大量使用了 immutable.js 結(jié)合 redux 來(lái)搭建業(yè)務(wù),與 React 中 PureComponnet 完美配合,性能保持非常好。但是在 React hooks 中再結(jié)合 typescript 它就顯得有點(diǎn)格格不入了,類型支持得不是很完美。這里可以嘗試一下 immer.js,引入成本小,寫法也簡(jiǎn)潔了不少。
最后推薦一個(gè)比較好用的 hooks 庫(kù) :ahooks : https://ahooks.js.org/zh-CN/hooks/async
ahooks 是一個(gè) React Hooks 庫(kù),致力提供常用且高質(zhì)量的 Hooks。針對(duì)性能優(yōu)化也提供了相應(yīng)的hooks, 例如 : usePersistFn useCreation ...
更多 hooks 的使用自行查看官方文檔吧,這里就不做過(guò)多的介紹了。
往期推薦
