React Hooks使用避坑指南
- 2021 年 6 月 20 日
- 筆記
函數組件比類組件更加方便實現業務邏輯程式碼的分離和組件的復用,函數組件也比類組件輕量,沒有react hooks之前,函數組件是無法實現LocalState的,這導致有localstate狀態的組件無法用函數組件來書寫,這限制了函數組件的應用範圍,而react hooks擴展了函數組件的能力。可是在使用的過程中,也要注意下面這些問題,否則就會掉進坑裡,造成性能損失。按照下面的方法做,,才能避開這些陷阱。
1. 將與狀態改變無關的變數和方法提取到組件函數外面
每次狀態改變時,整個函數組件都會重新執行一遍。導致函數組件內部定義的方法和變數,都會重新創建,重新給它們分配記憶體,這會導致性能受到影響。
import React, {useState,useCallback} from "react"; // 測試每次狀態改變時,方法是不是重新分配記憶體 let testFooMemoAlloc = new Set(); const Page = (props:any) => { console.log('每次狀態改變,函數組件從頭開始執行') const [count, setCount] = useState(0); const calc = () => { setCount(count + 1); } const bar = { a:1, b:2, c: '與狀態無關的變數定義' } const doFoo = () => { console.log('與狀態無關的方法'); } testFooMemoAlloc.add(doFoo) return ( <> <button onClick={calc}>加1</button> <p>count:{count}</p> <p>testFooMemoAlloc.size增加的話,說明每次都重新分配了記憶體:{testFooMemoAlloc.size}</p> </> ) } export default Page;
與改變狀態相關的變數和方法,必須放在hooks組件內,而無狀態無關的變數和方法,可以提取到函數組件外,避免每次狀態更新,都重新分配記憶體。也可以分別使用useMemo和useCallback包裹變數與函數,也能達到同樣的效果,後面會講。
import React, {useState,useCallback} from "react"; // 測試每次狀態改變時,方法是不是重新分配記憶體 let testFooMemoAlloc = new Set(); const bar = { a:1, b:2, c: '與狀態無關的變數定義' } const doFoo = () => { console.log('與狀態無關的方法'); } const Page = (props:any) => { console.log('每次狀態改變,函數組件從頭開始執行') const [count, setCount] = useState(0); const calc = () => { setCount(count + 1); } testFooMemoAlloc.add(doFoo) return ( <> <button onClick={calc}>加1</button> <p>count:{count}</p> <p>testFooMemoAlloc.size增加的話,說明每次都重新分配了記憶體:{testFooMemoAlloc.size}</p> </> ) } export default Page;
2. 用memo對子組件進行包裝
父組件引入子組件,會造成一些不必要的重複渲染,每次父組件更新count,子組件都會更新。
import React,{useState} from "react"; const Child = (props:any) => { console.log('子組件?') return( <div>我是一個子組件</div> ); } const Page = (props:any) => { const [count, setCount] = useState(0); return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <Child /> </> ) } export default Page;
使用memo,count變化子組件沒有更新
import React,{useState,memo} from "react"; const Child = memo((props:any) => { console.log('子組件?') return( <div>我是一個子組件</div> ); }) const Page = (props:any) => { const [count, setCount] = useState(0); return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <Child /> </> ) } export default Page;
給memo傳入第二個參數,開啟對象深度比較。當子組件傳遞的屬性值未發生改變時,子組件不會做無意義的render。
memo不僅適用於函數組件,也適用於class組件,是一個高階組件,默認情況下只會對複雜對象做淺層比較,如果想做深度比較,可以傳入第二個參數。與 shouldComponentUpdate
不同的是,deepCompare 返回 true
時,不會觸發 render,如果返回 false
,則會。而 shouldComponentUpdate
剛好與其相反。
import React, {useState, memo } from "react"; import deepCompare from "./deepCompare"; const Child = memo((props:any) => { console.log('子組件') return ( <> <div>我是一個子組件</div> <div>{ props.fooObj.a}</div> </> ); }, deepCompare) const Page = (props:any) => { const [count, setCount] = useState(0); const [fooObj, setFooObj] = useState({ a: 1, b: { c: 2 } }) console.log('頁面開始渲染') const calc = () => { setCount(count + 1); if (count === 3) { setFooObj({ b: { c: 2 }, a: count }) } } const doBar = () => { console.log('給子組件傳遞方法,測試一下是否會引起不必須的渲染') } return ( <> <button onClick={calc}>加1</button> <p>count:{count}</p> <Child fooObj={fooObj} doBar={doBar} /> </> ) } export default Page;
// 深度比較兩個對象是否相等 export default function deepCompare(prevProps: any, nextProps: any) { const len: number = arguments.length; let leftChain: any[] = []; let rightChain: any = []; // // console.log({ arguments }); // if (len < 2) { // console.log('需要傳入2個對象,才能進行兩個對象的屬性對比'); return true; } // for (let i = 1; i < len; i++) { // leftChain = []; // rightChain = []; console.log({ prevProps, nextProps }); if (!compare2Objects(prevProps, nextProps, leftChain, rightChain)) { // console.log('兩個對象不相等'); return false; } // } // console.log('兩個對象相等'); return true; } function compare2Objects(prevProps: any, nextProps: any, leftChain: any, rightChain: any) { var p; // 兩個值都為為NaN時,在js中是不相等的, 而在這裡認為相等才是合理的 if (isNaN(prevProps) && isNaN(nextProps) && typeof prevProps === 'number' && typeof nextProps === 'number') { return true; } // 原始值比較 if (prevProps === nextProps) { console.log('原始值', prevProps, nextProps); return true; } // 構造類型比較 if ( (typeof prevProps === 'function' && typeof nextProps === 'function') || (prevProps instanceof Date && nextProps instanceof Date) || (prevProps instanceof RegExp && nextProps instanceof RegExp) || (prevProps instanceof String && nextProps instanceof String) || (prevProps instanceof Number && nextProps instanceof Number) ) { console.log('function', prevProps.toString() === nextProps.toString()); return prevProps.toString() === nextProps.toString(); } // 兩個比較變數的值如果是null和undefined,在這裡會退出 if (!(prevProps instanceof Object && nextProps instanceof Object)) { console.log(prevProps, nextProps, 'prevProps instanceof Object && nextProps instanceof Object'); return false; } if (prevProps.isPrototypeOf(nextProps) || nextProps.isPrototypeOf(prevProps)) { console.log('prevProps.isPrototypeOf(nextProps) || nextProps.isPrototypeOf(prevProps)'); return false; } // 構造器不相等則兩個對象不相等 if (prevProps.constructor !== nextProps.constructor) { console.log('prevProps.constructor !== nextProps.constructor'); return false; } // 原型不相等則兩個對象不相等 if (prevProps.prototype !== nextProps.prototype) { console.log('prevProps.prototype !== nextProps.prototype'); return false; } if (leftChain.indexOf(prevProps) > -1 || rightChain.indexOf(nextProps) > -1) { console.log('leftChain.indexOf(prevProps) > -1 || rightChain.indexOf(nextProps) > -1'); return false; } // 遍歷下次的屬性對象,優先比較不相等的情形 for (p in nextProps) { if (nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)) { console.log('nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)'); return false; } else if (typeof nextProps[p] !== typeof prevProps[p]) { console.log('typeof nextProps[p] !== typeof prevProps[p]'); return false; } } // console.log('p in prevProps'); // 遍歷上次的屬性對象,優先比較不相等的情形 for (p in prevProps) { // 是否都存在某個屬性值 if (nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)) { console.log('nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)'); return false; } // 屬性值的類型是否相等 else if (typeof nextProps[p] !== typeof prevProps[p]) { console.log('typeof nextProps[p] !== typeof prevProps[p]'); return false; } console.log('typeof prevProps[p]', typeof prevProps[p]); switch (typeof prevProps[p]) { // 對象類型和函數類型的處理 case 'object': case 'function': leftChain.push(prevProps); rightChain.push(nextProps); if (!compare2Objects(prevProps[p], nextProps[p], leftChain, rightChain)) { console.log('!compare2Objects(prevProps[p], nextProps[p], leftChain, rightChain)'); return false; } leftChain.pop(); rightChain.pop(); break; default: // 基礎類型的處理 if (prevProps[p] !== nextProps[p]) { return false; } break; } } return true; }
3.用useCallback對組件方法進行包裝
當父組件傳遞方法給子組件的時候,memo好像沒什麼效果,無論是用const定義的方法,還在用箭頭函數或者bind定義的方法,子組件還是執行了
import React, { useState,memo } from 'react'; //子組件會有不必要渲染的例子 interface ChildProps { changeName: ()=>void; } const FunChild = ({ changeName}: ChildProps): JSX.Element => { console.log('普通函數子組件') return( <> <div>我是普通函數子組件</div> <button onClick={changeName}>普通函數子組件按鈕</button> </> ); } const FunMemo = memo(FunChild); const ArrowChild = ({ changeName}: ChildProps): JSX.Element => { console.log('箭頭函數子組件') return( <> <div>我是箭頭函數子組件</div> <button onClick={changeName.bind(null,'test')}>箭頭函數子組件按鈕</button> </> ); } const ArrowMemo = memo(ArrowChild); const BindChild = ({ changeName}: ChildProps): JSX.Element => { console.log('Bind函數子組件') return( <> <div>我是Bind函數子組件</div> <button onClick={changeName}>Bind函數子組件按鈕</button> </> ); } const BindMemo = memo(BindChild); const Page = (props:any) => { const [count, setCount] = useState(0); const name = "test"; const changeName = function() { console.log('測試給子組件傳遞方法,使用useCallback後,子組件是否還會進行無效渲染'); } return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <ArrowMemo changeName={()=>changeName()}/> <BindMemo changeName={changeName.bind(null)}/> <FunMemo changeName={changeName} /> </> ) } export default Page;
使用useCallback,參數為[],頁面初始渲染後,改變count的值,傳遞普通函數的子組件不再渲染, 傳遞箭頭函數和bind方式書寫的方法的子組件還是會渲染
import React, { useState,memo ,useCallback} from 'react'; //子組件會有不必要渲染的例子 interface ChildProps { changeName: ()=>void; } const FunChild = ({ changeName}: ChildProps): JSX.Element => { console.log('普通函數子組件') return( <> <div>我是普通函數子組件</div> <button onClick={changeName}>普通函數子組件按鈕</button> </> ); } const FunMemo = memo(FunChild); const ArrowChild = ({ changeName}: ChildProps): JSX.Element => { console.log('箭頭函數子組件') return( <> <div>我是箭頭函數子組件</div> <button onClick={changeName.bind(null,'test')}>箭頭函數子組件按鈕</button> </> ); } const ArrowMemo = memo(ArrowChild); const BindChild = ({ changeName}: ChildProps): JSX.Element => { console.log('Bind函數子組件') return( <> <div>我是Bind函數子組件</div> <button onClick={changeName}>Bind函數子組件按鈕</button> </> ); } const BindMemo = memo(BindChild); const Page = (props:any) => { const [count, setCount] = useState(0); const name = "test"; const changeName = useCallback(() => { console.log('測試給子組件傳遞方法,使用useCallback後,子組件是否還會進行無效渲染'); },[]) return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <ArrowMemo changeName={()=>changeName()}/> <BindMemo changeName={changeName.bind(null)}/> <FunMemo changeName={changeName} /> </> ) } export default Page;
4.用useMemo對組件中的對象變數進行包裝
在子組件使用了memo,useCallback的情況下,給子組件傳遞一個對象屬性,對象值和方法都未發生改變的情況下,父組件無關狀態變更,子組件也會重新渲染。
import React, { useState,memo ,useCallback} from 'react'; //子組件會有不必要渲染的例子-使用了memo,useCallback的情況下,給子組件傳遞一個對象屬性值 interface ChildProps { childStyle: { color: string; fontSize: string;}; changeName: ()=>void; } const FunChild = ({ childStyle,changeName}: ChildProps): JSX.Element => { console.log('普通函數子組件') return( <> <div style={childStyle}>我是普通函數子組件</div> <button onClick={changeName}>普通函數子組件按鈕</button> </> ); } const FunMemo = memo(FunChild); const Page = (props:any) => { const [count, setCount] = useState(0); const childStyle = {color:'green',fontSize:'16px'}; const changeName = useCallback(() => { console.log('測試給子組件傳遞方法,使用useCallback後,子組件是否還會進行無效渲染'); },[]) return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <FunMemo childStyle={childStyle} changeName={changeName} /> </> ) } export default Page;
使用useMemo可以解決給子組件傳遞對象屬性時的不必要更新問題。
import React, { useState,memo, useMemo, useCallback} from 'react'; //子組件會有不必要渲染的例子 interface ChildProps { childStyle: { color: string; fontSize: string;}; changeName: ()=>void; } const FunChild = ({ childStyle,changeName}: ChildProps): JSX.Element => { console.log('普通函數子組件') return( <> <div style={childStyle}>我是普通函數子組件</div> <button onClick={changeName}>普通函數子組件按鈕</button> </> ); } const FunMemo = memo(FunChild); const Page = (props:any) => { const [count, setCount] = useState(0); const [name, setName] = useState(""); const childStyle = {color:'green',fontSize:'16px'}; const changeName = useCallback(() => { setName('變一下名稱') }, []) const childStyleMemo = useMemo(() => { return { color: name === '變一下名稱' ? 'red':'green', fontSize: '16px' } }, [name]) return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <FunMemo childStyle={childStyleMemo} changeName={changeName} /> </> ) } export default Page;