記一個(gè)React.memo引起的bug
與PureComponent不同的是PureComponent只是進(jìn)行淺對(duì)比props來(lái)決定是否跳過(guò)更新數(shù)據(jù)這個(gè)步驟,memo可以自己決定是否更新,但它是一個(gè)函數(shù)組件而非一個(gè)類,但請(qǐng)不要依賴它來(lái)“阻止”渲染,因?yàn)檫@會(huì)產(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)問(wèn)題描述
我們?cè)谔幚順I(yè)務(wù)需求時(shí),會(huì)用到memo來(lái)優(yōu)化組件的渲染,例如某個(gè)組件依賴自身的狀態(tài)即可完成更新,或僅在props中的某些數(shù)據(jù)變更時(shí)才需要重新渲染,那么我們就可以使用memo包裹住目標(biāo)組件,這樣在props沒(méi)有變更時(shí),組件不會(huì)重新渲染,以此來(lái)規(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í),組件才會(huì)重新渲染。
在下面是我們用到這個(gè)組件的場(chǎng)景。
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中沒(méi)有提供,但是展示需要的數(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傳過(guò)來(lái)的列表,但是列表中有些數(shù)據(jù)服務(wù)端沒(méi)有給到,需要你再次通過(guò)另一個(gè)接口去獲取,我用settimeout替代了獲取接口數(shù)據(jù)的過(guò)程。為了讓用戶在獲取接口的過(guò)程中不需要等待,我們先根據(jù)props的數(shù)據(jù)給items設(shè)置了默認(rèn)值。然后在接口數(shù)據(jù)拿到后再更新items。
但幾秒鐘后我們?cè)谧咏M件InputNumber中更新數(shù)據(jù),會(huì)看到:

selected依然是false!
這是為什么呢?前面不是把items中所有的selected都改為true了嗎?
我們?cè)俅蛴∫幌耰tems看看:

似乎在InputNumber中的items依然是初始值。
對(duì)于這一現(xiàn)象,我個(gè)人理解為memo使用的memoization算法存儲(chǔ)了上一次渲染的items數(shù)值,由于InputNumber沒(méi)有重新渲染,所以在它的本地狀態(tài)中,items一直是初始值。
解決方法
方案一. 使用useRef + forceUpdate方案
我們可以使用useRef來(lái)保證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中沒(méi)有提供,但是展示需要的數(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í)候我們會(huì)看到

items中的selected已經(jīng)變成true了
但是此時(shí)如果我們需要根據(jù)items中的selected去渲染不同的文字,會(huì)發(fā)現(xiàn)并沒(méi)有變化。
? return (
? ? <View className={styles.brandBox}>
? ? ? {items.current.map((item: GoodSku, index: number) => {
? ? ? ? return (
? ? ? ? ? <View key={item.skuId}>
? ? ? ? ? ? <View>{item.selected ? '選中' : '未選中'}</View>
? ? ? ? ? ? <InputNumber
? ? ? ? ? ? ? inputDisable
? ? ? ? ? ? ? // 最小購(gòu)買數(shù)量
? ? ? ? ? ? ? min={0}
? ? ? ? ? ? ? max={50}
? ? ? ? ? ? ? value={item.num}
? ? ? ? ? ? ? onChange={() => {
? ? ? ? ? ? ? ? console.log('selected', items)
? ? ? ? ? ? ? }}
? ? ? ? ? ? />
? ? ? ? ? </View>
? ? ? ? )
? ? ? })}
? ? </View>
? )顯示還是未選中

這是因?yàn)閡seRef的值會(huì)更新,但不會(huì)更新他們的 UI,除非組件重新渲染。因此我們可以手動(dòng)更新一個(gè)值去強(qiáng)制讓組件在我們需要的時(shí)候重新渲染。
const CartBrand: FC<Props> = (props: Props) => {
? const { info } = props
? // 定義一個(gè)state,它在每次調(diào)用的時(shí)候都會(huì)讓組件重新渲染
? 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
? ? ? ? ? ? ? // 最小購(gòu)買數(shù)量
? ? ? ? ? ? ? min={0}
? ? ? ? ? ? ? max={50}
? ? ? ? ? ? ? value={item.num}
? ? ? ? ? ? ? onChange={() => {
? ? ? ? ? ? ? ? console.log('selected', items)
? ? ? ? ? ? ? }}
? ? ? ? ? ? />
? ? ? ? ? </View>
? ? ? ? )
? ? ? })}
? ? </View>
? )
}
export default CartBrand這樣我們就可以使用最新的items,并保證items相關(guān)的渲染不會(huì)出錯(cuò)
方案2. 使用useCallback
在InputNumber這個(gè)組件中,memo的第二個(gè)參數(shù),我沒(méi)有判斷onClick回調(diào)是否相同,因?yàn)闊o(wú)論如何它都是不同的。
參考這個(gè)文章:use react memo wisely
函數(shù)對(duì)象只等于它自己。讓我們通過(guò)比較一些函數(shù)來(lái)看看:
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); // => truesumFactory()是一個(gè)工廠函數(shù)。它返回對(duì) 2 個(gè)數(shù)字求和的函數(shù)。
函數(shù)sum1和sum2由工廠創(chuàng)建。這兩個(gè)函數(shù)對(duì)數(shù)字求和。但是,sum1和sum2是不同的函數(shù)對(duì)象(sum1 === sum2is false)。
每次父組件為其子組件定義回調(diào)時(shí),它都會(huì)創(chuàng)建新的函數(shù)實(shí)例。在自定義比較函數(shù)中過(guò)濾掉onClick固然可以規(guī)避掉這種問(wèn)題,但是這也會(huì)導(dǎo)致我們上述的問(wèn)題,在前面提到的文章中,為我們提供了另一種解決思路,我們可以使用useCallback來(lái)緩存回調(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)前購(gòu)物車中所有的商品的庫(kù)存狀態(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
? ? ? ? ? ? ? // 最小購(gòu)買數(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也會(huì)刷新,不過(guò)在復(fù)雜的邏輯中,比如items的結(jié)構(gòu)非常復(fù)雜,items中很多字段都會(huì)有高頻率的改變,那這種方式會(huì)減弱InputNumber中memo的效果,因?yàn)樗鼤?huì)隨著items的改變而刷新。
總結(jié)
在最后,我還是選擇了方案一解決這個(gè)問(wèn)題。同時(shí)提醒自己,memo的使用要謹(jǐn)慎??
到此這篇關(guān)于記一個(gè)React.memo引起的bug的文章就介紹到這了,更多相關(guān)React memo bug內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
TypeScript在React中的應(yīng)用技術(shù)實(shí)例解析
這篇文章主要為大家介紹了TypeScript在React中的應(yīng)用技術(shù)實(shí)例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
react?native?reanimated實(shí)現(xiàn)動(dòng)畫示例詳解
這篇文章主要為大家介紹了react?native?reanimated實(shí)現(xiàn)動(dòng)畫示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
關(guān)于React動(dòng)態(tài)加載路由處理的相關(guān)問(wèn)題
這篇文章主要介紹了關(guān)于React動(dòng)態(tài)加載路由處理的相關(guān)問(wèn)題,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
React-redux?中useSelector使用源碼分析
在一個(gè) action 被分發(fā)(dispatch) 后,useSelector() 默認(rèn)對(duì) select 函數(shù)的返回值進(jìn)行引用比較 ===,并且僅在返回值改變時(shí)觸發(fā)重渲染,,這篇文章主要介紹了React-redux?中useSelector使用,需要的朋友可以參考下2023-10-10
解析react?函數(shù)組件輸入卡頓問(wèn)題?usecallback?react.memo
useMemo是一個(gè)react hook,我們可以使用它在組件中包裝函數(shù)??梢允褂盟鼇?lái)確保該函數(shù)中的值僅在依賴項(xiàng)之一發(fā)生變化時(shí)才重新計(jì)算,這篇文章主要介紹了react?函數(shù)組件輸入卡頓問(wèn)題?usecallback?react.memo,需要的朋友可以參考下2022-07-07
詳解create-react-app 2.0版本如何啟用裝飾器語(yǔ)法
這篇文章主要介紹了詳解create-react-app 2.0版本如何啟用裝飾器語(yǔ)法,cra2.0時(shí)代如何啟用裝飾器語(yǔ)法呢? 我們依舊采用的是react-app-rewired, 通過(guò)劫持webpack cofig對(duì)象, 達(dá)到修改的目的2018-10-10

