您好,登錄后才能下訂單哦!
這篇文章主要介紹如何解決React.memo引起的bug問題,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
與PureComponent不同的是PureComponent只是進行淺對比props來決定是否跳過更新數據這個步驟,memo可以自己決定是否更新,但它是一個函數組件而非一個類,但請不要依賴它來“阻止”渲染,因為這會產生 bug。
import React from "react"; function MyComponent({props}){ console.log('111); return ( <div> {props} </div> ) }; function areEqual(prevProps, nextProps) { if(prevProps.seconds===nextProps.seconds){ return true }else { return false } } export default React.memo(MyComponent,areEqual)
我們在處理業務需求時,會用到memo來優化組件的渲染,例如某個組件依賴自身的狀態即可完成更新,或僅在props中的某些數據變更時才需要重新渲染,那么我們就可以使用memo包裹住目標組件,這樣在props沒有變更時,組件不會重新渲染,以此來規避不必要的重復渲染。
下面是我創建的一個公共組件:
type Props = { inputDisable?: boolean // 是否一直展示輸入框 inputVisible?: boolean value: any min: number max: number onChange: (v: number) => void } const InputNumber: FC<Props> = memo( (props: Props) => { const { inputDisable, max, min, value, inputVisible } = props const handleUpdate = (e: any, num) => { e.stopPropagation() props.onChange(num) } return ( <View className={styles.inputNumer}> {(value !== 0 || inputVisible) && ( <> <Image className={styles.btn} src={require(value <= min ? '../../assets/images/reduce-no.png' : '../../assets/images/reduce.png')} onClick={e => handleUpdate(e, value - 1)} mode='aspectFill' /> <Input value={value} disabled={inputDisable} alwaysEmbed type='number' cursor={-1} onInput={e => handleUpdate(e, parseInt(e.detail.value ? e.detail.value : '0'), 'input')} /> </> )} <Image className={styles.btn} src={require(max !== -1 && (value >= max || min > max) ? '../../assets/images/plus-no.png' : '../../assets/images/plus.png')} onClick={e => handleUpdate(e, value + 1)} /> </View> ) }, (prevProps, nextProps) => { return prevProps.value === nextProps.value && prevProps.min === nextProps.min && prevProps.max === nextProps.max } ) export default InputNumber
這個組件是一個自定義的數字選擇器,在memo的第二個參數中設置我們需要的參數,當這些參數有變更時,組件才會重新渲染。
在下面是我們用到這個組件的場景。
type Props = { info: any onUpdate: (items) => void } const CartBrand: FC<Props> = (props: Props) => { const { info } = props const [items, setItems] = useState<any>( info.items.map(item => { // selected默認為false return { num:1, selected: false } }) ) useEffect(() => { getCartStatus() }, []) // 獲取info.items中沒有提供,但是展示需要的數據 const getCartStatus = () => { setTimeout(() => { setItems( info.items.map(item => { //更新selected為true return {num: 1, selected: true } }) ) }, 1000) } return ( <View className={styles.brandBox}> {items.map((item: GoodSku, index: number) => { return ( <InputNumber key={item.skuId} inputDisable min={0} max={50} value={item.num} onChange={v => { console.log(v, item.selected) }} /> ) })} </View> ) } export default CartBrand
這個組件的目的是展示props傳過來的列表,但是列表中有些數據服務端沒有給到,需要你再次通過另一個接口去獲取,我用settimeout替代了獲取接口數據的過程。為了讓用戶在獲取接口的過程中不需要等待,我們先根據props的數據給items設置了默認值。然后在接口數據拿到后再更新items。
但幾秒鐘后我們在子組件InputNumber中更新數據,會看到:
selected依然是false!
這是為什么呢?前面不是把items中所有的selected都改為true了嗎?
我們再打印一下items看看:
似乎在InputNumber中的items依然是初始值。
對于這一現象,我個人理解為memo使用的memoization算法存儲了上一次渲染的items數值,由于InputNumber沒有重新渲染,所以在它的本地狀態中,items一直是初始值。
我們可以使用useRef來保證items一直是最新的,講useState換為useRef
type Props = { info: any onUpdate: (items) => void } const CartBrand: FC<Props> = (props: Props) => { const { info } = props const items = useRef<any>( info.items.map(item => { // selected默認為false return { num:1, selected: false } }) ) useEffect(() => { getCartStatus() }, []) // 獲取info.items中沒有提供,但是展示需要的數據 const getCartStatus = () => { setTimeout(() => { items.current = info.items.map(() => { return { num: 1, selected: true } }) }, 1000) } return ( <View className={styles.brandBox}> {items.current.map((item: GoodSku, index: number) => { return ( <InputNumber key={item.skuId} inputDisable min={0} max={50} value={item.num} onChange={v => { console.log(v, items) }} /> ) })} </View> ) } export default CartBrand
這樣再打印的時候我們會看到
items中的selected已經變成true了
但是此時如果我們需要根據items中的selected去渲染不同的文字,會發現并沒有變化。
return ( <View className={styles.brandBox}> {items.current.map((item: GoodSku, index: number) => { return ( <View key={item.skuId}> <View>{item.selected ? '選中' : '未選中'}</View> <InputNumber inputDisable // 最小購買數量 min={0} max={50} value={item.num} onChange={() => { console.log('selected', items) }} /> </View> ) })} </View> )
顯示還是未選中
這是因為useRef的值會更新,但不會更新他們的 UI,除非組件重新渲染。因此我們可以手動更新一個值去強制讓組件在我們需要的時候重新渲染。
const CartBrand: FC<Props> = (props: Props) => { const { info } = props // 定義一個state,它在每次調用的時候都會讓組件重新渲染 const [, setForceUpdate] = useState(Date.now()) const items = useRef<any>( info.items.map(item => { return { num: 1, selected: false } }) ) useEffect(() => { getCartStatus() }, []) const getCartStatus = () => { setTimeout(() => { items.current = info.items.map(() => { return { num: 1, selected: true } }) setForceUpdate() }, 5000) } return ( <View className={styles.brandBox}> {items.current.map((item: GoodSku, index: number) => { return ( <View key={item.skuId}> <View>{item.selected ? '選中' : '未選中'}</View> <InputNumber inputDisable // 最小購買數量 min={0} max={50} value={item.num} onChange={() => { console.log('selected', items) }} /> </View> ) })} </View> ) } export default CartBrand
這樣我們就可以使用最新的items,并保證items相關的渲染不會出錯
在InputNumber這個組件中,memo的第二個參數,我沒有判斷onClick回調是否相同,因為無論如何它都是不同的。
參考這個文章:use react memo wisely
函數對象只等于它自己。讓我們通過比較一些函數來看看:
function sumFactory() { return (a, b) => a + b; } const sum1 = sumFactory(); const sum2 = sumFactory(); console.log(sum1 === sum2); // => false console.log(sum1 === sum1); // => true console.log(sum2 === sum2); // => true
sumFactory()是一個工廠函數。它返回對 2 個數字求和的函數。
函數sum1和sum2由工廠創建。這兩個函數對數字求和。但是,sum1和sum2是不同的函數對象(sum1 === sum2is false)。
每次父組件為其子組件定義回調時,它都會創建新的函數實例。在自定義比較函數中過濾掉onClick固然可以規避掉這種問題,但是這也會導致我們上述的問題,在前面提到的文章中,為我們提供了另一種解決思路,我們可以使用useCallback來緩存回調函數:
type Props = { info: any onUpdate: (items) => void } const CartBrand: FC<Props> = (props: Props) => { const { info } = props const [items, setItems] = useState( info.items.map(item => { return { num: 1, selected: false } }) ) useEffect(() => { getCartStatus() }, []) // 獲取當前購物車中所有的商品的庫存狀態 const getCartStatus = () => { setTimeout(() => { setItems( info.items.map(() => { return { num: 1, selected: true } }) ) }, 5000) } // 使用useCallback緩存回調函數 const logChange = useCallback( v => { console.log('selected', items) }, [items] ) return ( <View className={styles.brandBox}> {items.map((item: GoodSku, index: number) => { return ( <View key={item.skuId}> <InputNumber inputDisable // 最小購買數量 min={0} max={50} value={item.num} onChange={logChange} /> </View> ) })} </View> ) }
相應的,我們可以把InputNumber的自定義比較函數去掉。
type Props = { inputDisable?: boolean // 是否一直展示輸入框 inputVisible?: boolean value: any min: number max: number onChange: (v: number) => void } const InputNumber: FC<Props> = memo( (props: Props) => { const { inputDisable, max, min, value, inputVisible } = props const handleUpdate = (e: any, num) => { e.stopPropagation() props.onChange(num) } return ( <View className={styles.inputNumer}> {(value !== 0 || inputVisible) && ( <> <Image className={styles.btn} src={require(value <= min ? '../../assets/images/reduce-no.png' : '../../assets/images/reduce.png')} onClick={e => handleUpdate(e, value - 1)} mode='aspectFill' /> <Input value={value} disabled={inputDisable} alwaysEmbed type='number' cursor={-1} onInput={e => handleUpdate(e, parseInt(e.detail.value ? e.detail.value : '0'), 'input')} /> </> )} <Image className={styles.btn} src={require(max !== -1 && (value >= max || min > max) ? '../../assets/images/plus-no.png' : '../../assets/images/plus.png')} onClick={e => handleUpdate(e, value + 1)} /> </View> ) } ) export default InputNumber
這樣在items更新的時候,inputNumber也會刷新,不過在復雜的邏輯中,比如items的結構非常復雜,items中很多字段都會有高頻率的改變,那這種方式會減弱InputNumber中memo的效果,因為它會隨著items的改變而刷新。
以上是“如何解決React.memo引起的bug問題”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。