記一個(gè)React.memo引起的bug
與PureComponent不同的是PureComponent只是進(jìn)行淺對比props來決定是否跳過更新數(shù)據(jù)這個(gè)步驟,memo可以自己決定是否更新,但它是一個(gè)函數(shù)組件而非一個(gè)類,但請不要依賴它來“阻止”渲染,因?yàn)檫@會產(chǎn)生 bug。
一般memo用法:
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)
問題描述
我們在處理業(yè)務(wù)需求時(shí),會用到memo來優(yōu)化組件的渲染,例如某個(gè)組件依賴自身的狀態(tài)即可完成更新,或僅在props中的某些數(shù)據(jù)變更時(shí)才需要重新渲染,那么我們就可以使用memo包裹住目標(biāo)組件,這樣在props沒有變更時(shí),組件不會重新渲染,以此來規(guī)避不必要的重復(fù)渲染。
下面是我創(chuàng)建的一個(gè)公共組件:
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
這個(gè)組件是一個(gè)自定義的數(shù)字選擇器,在memo的第二個(gè)參數(shù)中設(shè)置我們需要的參數(shù),當(dāng)這些參數(shù)有變更時(shí),組件才會重新渲染。
在下面是我們用到這個(gè)組件的場景。
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默認(rèn)為false ? ? return { num:1, selected: false } ? }) ) useEffect(() => { ? getCartStatus() }, []) // 獲取info.items中沒有提供,但是展示需要的數(shù)據(jù) 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
這個(gè)組件的目的是展示props傳過來的列表,但是列表中有些數(shù)據(jù)服務(wù)端沒有給到,需要你再次通過另一個(gè)接口去獲取,我用settimeout替代了獲取接口數(shù)據(jù)的過程。為了讓用戶在獲取接口的過程中不需要等待,我們先根據(jù)props的數(shù)據(jù)給items設(shè)置了默認(rèn)值。然后在接口數(shù)據(jù)拿到后再更新items。
但幾秒鐘后我們在子組件InputNumber中更新數(shù)據(jù),會看到:
selected依然是false!
這是為什么呢?前面不是把items中所有的selected都改為true了嗎?
我們再打印一下items看看:
似乎在InputNumber中的items依然是初始值。
對于這一現(xiàn)象,我個(gè)人理解為memo使用的memoization算法存儲了上一次渲染的items數(shù)值,由于InputNumber沒有重新渲染,所以在它的本地狀態(tài)中,items一直是初始值。
解決方法
方案一. 使用useRef + forceUpdate方案
我們可以使用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默認(rèn)為false ? ? ? return { num:1, selected: false } ? ? }) ? ) ? useEffect(() => { ? ? getCartStatus() ? }, []) ?? ? // 獲取info.items中沒有提供,但是展示需要的數(shù)據(jù) ? 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
這樣再打印的時(shí)候我們會看到
items中的selected已經(jīng)變成true了
但是此時(shí)如果我們需要根據(jù)items中的selected去渲染不同的文字,會發(fā)現(xiàn)并沒有變化。
? return ( ? ? <View className={styles.brandBox}> ? ? ? {items.current.map((item: GoodSku, index: number) => { ? ? ? ? return ( ? ? ? ? ? <View key={item.skuId}> ? ? ? ? ? ? <View>{item.selected ? '選中' : '未選中'}</View> ? ? ? ? ? ? <InputNumber ? ? ? ? ? ? ? inputDisable ? ? ? ? ? ? ? // 最小購買數(shù)量 ? ? ? ? ? ? ? min={0} ? ? ? ? ? ? ? max={50} ? ? ? ? ? ? ? value={item.num} ? ? ? ? ? ? ? onChange={() => { ? ? ? ? ? ? ? ? console.log('selected', items) ? ? ? ? ? ? ? }} ? ? ? ? ? ? /> ? ? ? ? ? </View> ? ? ? ? ) ? ? ? })} ? ? </View> ? )
顯示還是未選中
這是因?yàn)閡seRef的值會更新,但不會更新他們的 UI,除非組件重新渲染。因此我們可以手動更新一個(gè)值去強(qiáng)制讓組件在我們需要的時(shí)候重新渲染。
const CartBrand: FC<Props> = (props: Props) => { ? const { info } = props ? // 定義一個(gè)state,它在每次調(diào)用的時(shí)候都會讓組件重新渲染 ? 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 ? ? ? ? ? ? ? // 最小購買數(shù)量 ? ? ? ? ? ? ? min={0} ? ? ? ? ? ? ? max={50} ? ? ? ? ? ? ? value={item.num} ? ? ? ? ? ? ? onChange={() => { ? ? ? ? ? ? ? ? console.log('selected', items) ? ? ? ? ? ? ? }} ? ? ? ? ? ? /> ? ? ? ? ? </View> ? ? ? ? ) ? ? ? })} ? ? </View> ? ) } export default CartBrand
這樣我們就可以使用最新的items,并保證items相關(guān)的渲染不會出錯(cuò)
方案2. 使用useCallback
在InputNumber這個(gè)組件中,memo的第二個(gè)參數(shù),我沒有判斷onClick回調(diào)是否相同,因?yàn)闊o論如何它都是不同的。
參考這個(gè)文章:use react memo wisely
函數(shù)對象只等于它自己。讓我們通過比較一些函數(shù)來看看:
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()是一個(gè)工廠函數(shù)。它返回對 2 個(gè)數(shù)字求和的函數(shù)。
函數(shù)sum1和sum2由工廠創(chuàng)建。這兩個(gè)函數(shù)對數(shù)字求和。但是,sum1和sum2是不同的函數(shù)對象(sum1 === sum2is false)。
每次父組件為其子組件定義回調(diào)時(shí),它都會創(chuàng)建新的函數(shù)實(shí)例。在自定義比較函數(shù)中過濾掉onClick固然可以規(guī)避掉這種問題,但是這也會導(dǎo)致我們上述的問題,在前面提到的文章中,為我們提供了另一種解決思路,我們可以使用useCallback來緩存回調(diào)函數(shù):
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() ? }, []) ? // 獲取當(dāng)前購物車中所有的商品的庫存狀態(tài) ? const getCartStatus = () => { ? ? setTimeout(() => { ? ? ? setItems( ? ? ? ? info.items.map(() => { ? ? ? ? ? return { num: 1, selected: true } ? ? ? ? }) ? ? ? ) ? ? }, 5000) ? } ? // 使用useCallback緩存回調(diào)函數(shù) ? 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 ? ? ? ? ? ? ? // 最小購買數(shù)量 ? ? ? ? ? ? ? min={0} ? ? ? ? ? ? ? max={50} ? ? ? ? ? ? ? value={item.num} ? ? ? ? ? ? ? onChange={logChange} ? ? ? ? ? ? /> ? ? ? ? ? </View> ? ? ? ? ) ? ? ? })} ? ? </View> ? ) }
相應(yīng)的,我們可以把InputNumber的自定義比較函數(shù)去掉。
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更新的時(shí)候,inputNumber也會刷新,不過在復(fù)雜的邏輯中,比如items的結(jié)構(gòu)非常復(fù)雜,items中很多字段都會有高頻率的改變,那這種方式會減弱InputNumber中memo的效果,因?yàn)樗鼤S著items的改變而刷新。
總結(jié)
在最后,我還是選擇了方案一解決這個(gè)問題。同時(shí)提醒自己,memo的使用要謹(jǐn)慎??
到此這篇關(guān)于記一個(gè)React.memo引起的bug的文章就介紹到這了,更多相關(guān)React memo bug內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React Native中TabBarIOS的簡單使用方法示例
最近在學(xué)習(xí)過程中遇到了很多問題,TabBarIOS的使用就是一個(gè),所以下面這篇文章主要給大家介紹了關(guān)于React Native中TabBarIOS簡單使用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-10-10Ant Design與Ant Design pro入門使用教程
Ant Design 是一個(gè)服務(wù)于企業(yè)級產(chǎn)品的設(shè)計(jì)體系,組件庫是它的 React 實(shí)現(xiàn),antd 被發(fā)布為一個(gè) npm 包方便開發(fā)者安裝并使用,這篇文章主要介紹了Ant Design與Ant Design pro入門,需要的朋友可以參考下2023-12-12React中hook函數(shù)與useState及useEffect的使用
這篇文章主要介紹了React中hook函數(shù)與useState及useEffect的使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-10-10React在定時(shí)器中無法獲取狀態(tài)最新值的問題
這篇文章主要介紹了React在定時(shí)器中無法獲取狀態(tài)最新值的問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08React實(shí)現(xiàn)多個(gè)場景下鼠標(biāo)跟隨提示框詳解
這篇文章主要為大家介紹了React實(shí)現(xiàn)多個(gè)場景下鼠標(biāo)跟隨提示框詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09React父組件調(diào)用子組件中的方法實(shí)例詳解
最近做一個(gè)React項(xiàng)目,所有組件都使用了函數(shù)式組件,遇到一個(gè)父組件調(diào)用子組件方法的問題,下面這篇文章主要給大家介紹了關(guān)于React父組件調(diào)用子組件中方法的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07