经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » React » 查看文章
记一个React.memo引起的bug
来源:jb51  时间:2022/3/8 10:54:55  对本文有异议

与PureComponent不同的是PureComponent只是进行浅对比props来决定是否跳过更新数据这个步骤,memo可以自己决定是否更新,但它是一个函数组件而非一个类,但请不要依赖它来“阻止”渲染,因为这会产生 bug。

一般memo用法:

  1. import React from "react";
  2.  
  3. function MyComponent({props}){
  4. ? ? console.log('111);
  5. ? ? return (
  6. ? ? ? ? <div> {props} </div>
  7. ? ? )
  8. };
  9.  
  10. function areEqual(prevProps, nextProps) {
  11. ? ? if(prevProps.seconds===nextProps.seconds){
  12. ? ? ? ? return true
  13. ? ? }else {
  14. ? ? ? ? return false
  15. ? ? }
  16.  
  17. }
  18. export default React.memo(MyComponent,areEqual)

问题描述

我们在处理业务需求时,会用到memo来优化组件的渲染,例如某个组件依赖自身的状态即可完成更新,或仅在props中的某些数据变更时才需要重新渲染,那么我们就可以使用memo包裹住目标组件,这样在props没有变更时,组件不会重新渲染,以此来规避不必要的重复渲染。
下面是我创建的一个公共组件:

  1. type Props = {
  2. ?inputDisable?: boolean
  3. ?// 是否一直展示输入框
  4. ?inputVisible?: boolean
  5. ?value: any
  6. ?min: number
  7. ?max: number
  8. ?onChange: (v: number) => void
  9. }
  10.  
  11. const InputNumber: FC<Props> = memo(
  12. ?(props: Props) => {
  13. ? ?const { inputDisable, max, min, value, inputVisible } = props
  14.  
  15. ? ?const handleUpdate = (e: any, num) => {
  16. ? ? ?e.stopPropagation()
  17. ? ? ?props.onChange(num)
  18. ? ?}
  19. ? ?return (
  20. ? ? ?<View className={styles.inputNumer}>
  21. ? ? ? ?{(value !== 0 || inputVisible) && (
  22. ? ? ? ? ?<>
  23. ? ? ? ? ? ?<Image
  24. ? ? ? ? ? ? ?className={styles.btn}
  25. ? ? ? ? ? ? ?src={require(value <= min
  26. ? ? ? ? ? ? ? ?? '../../assets/images/reduce-no.png'
  27. ? ? ? ? ? ? ? ?: '../../assets/images/reduce.png')}
  28. ? ? ? ? ? ? ?onClick={e => handleUpdate(e, value - 1)}
  29. ? ? ? ? ? ? ?mode='aspectFill'
  30. ? ? ? ? ? ?/>
  31. ? ? ? ? ? ?<Input
  32. ? ? ? ? ? ? ?value={value}
  33. ? ? ? ? ? ? ?disabled={inputDisable}
  34. ? ? ? ? ? ? ?alwaysEmbed
  35. ? ? ? ? ? ? ?type='number'
  36. ? ? ? ? ? ? ?cursor={-1}
  37. ? ? ? ? ? ? ?onInput={e => handleUpdate(e, parseInt(e.detail.value ? e.detail.value : '0'), 'input')}
  38. ? ? ? ? ? ?/>
  39. ? ? ? ? ?</>
  40. ? ? ? ?)}
  41. ? ? ? ?<Image
  42. ? ? ? ? ?className={styles.btn}
  43. ? ? ? ? ?src={require(max !== -1 && (value >= max || min > max)
  44. ? ? ? ? ? ?? '../../assets/images/plus-no.png'
  45. ? ? ? ? ? ?: '../../assets/images/plus.png')}
  46. ? ? ? ? ?onClick={e => handleUpdate(e, value + 1)}
  47. ? ? ? ?/>
  48. ? ? ?</View>
  49. ? ?)
  50. ?},
  51. ?(prevProps, nextProps) => {
  52. ? ?return prevProps.value === nextProps.value && prevProps.min === nextProps.min && prevProps.max === nextProps.max
  53. ?}
  54. )
  55.  
  56. export default InputNumber

这个组件是一个自定义的数字选择器,在memo的第二个参数中设置我们需要的参数,当这些参数有变更时,组件才会重新渲染。
在下面是我们用到这个组件的场景。

  1. type Props = {
  2. info: any
  3. onUpdate: (items) => void
  4. }
  5.  
  6. const CartBrand: FC<Props> = (props: Props) => {
  7. const { info } = props
  8. const [items, setItems] = useState<any>(
  9. ? info.items.map(item => {
  10. ? // selected默认为false
  11. ? ? return { num:1, selected: false }
  12. ? })
  13. )
  14.  
  15. useEffect(() => {
  16. ? getCartStatus()
  17. }, [])
  18.  
  19. // 获取info.items中没有提供,但是展示需要的数据
  20. const getCartStatus = () => {
  21. ? setTimeout(() => {
  22. ? ? setItems(
  23. ? ? ? info.items.map(item => {
  24. ? ? ? //更新selected为true
  25. ? ? ? ? return {num: 1, selected: true }
  26. ? ? ? })
  27. ? ? )
  28. ? }, 1000)
  29. }
  30.  
  31. return (
  32. ? <View className={styles.brandBox}>
  33. ? ? {items.map((item: GoodSku, index: number) => {
  34. ? ? ? return (
  35. ? ? ? ? <InputNumber
  36. ? ? ? ? ? key={item.skuId}
  37. ? ? ? ? ? inputDisable
  38. ? ? ? ? ? min={0}
  39. ? ? ? ? ? max={50}
  40. ? ? ? ? ? value={item.num}
  41. ? ? ? ? ? onChange={v => {
  42. ? ? ? ? ? ? console.log(v, item.selected)
  43. ? ? ? ? ? }}
  44. ? ? ? ? />
  45. ? ? ? )
  46. ? ? })}
  47. ? </View>
  48. )
  49. }
  50.  
  51. export default CartBrand

这个组件的目的是展示props传过来的列表,但是列表中有些数据服务端没有给到,需要你再次通过另一个接口去获取,我用settimeout替代了获取接口数据的过程。为了让用户在获取接口的过程中不需要等待,我们先根据props的数据给items设置了默认值。然后在接口数据拿到后再更新items。
但几秒钟后我们在子组件InputNumber中更新数据,会看到:

selected依然是false!
这是为什么呢?前面不是把items中所有的selected都改为true了吗?
我们再打印一下items看看:

似乎在InputNumber中的items依然是初始值。
对于这一现象,我个人理解为memo使用的memoization算法存储了上一次渲染的items数值,由于InputNumber没有重新渲染,所以在它的本地状态中,items一直是初始值。

解决方法

方案一. 使用useRef + forceUpdate方案

我们可以使用useRef来保证items一直是最新的,讲useState换为useRef

  1. ? type Props = {
  2. ? info: any
  3. ? onUpdate: (items) => void
  4. }
  5.  
  6. const CartBrand: FC<Props> = (props: Props) => {
  7. ? const { info } = props
  8. ? const items = useRef<any>(
  9. ? ? info.items.map(item => {
  10. ? ? // selected默认为false
  11. ? ? ? return { num:1, selected: false }
  12. ? ? })
  13. ? )
  14.  
  15. ? useEffect(() => {
  16. ? ? getCartStatus()
  17. ? }, [])
  18. ??
  19. ? // 获取info.items中没有提供,但是展示需要的数据
  20. ? const getCartStatus = () => {
  21. ? ? setTimeout(() => {
  22. ? ? ? items.current = info.items.map(() => {
  23. ? ? ? ? return { num: 1, selected: true }
  24. ? ? ? })
  25. ? ? }, 1000)
  26. ? }
  27.  
  28. ? return (
  29. ? ? <View className={styles.brandBox}>
  30. ? ? ? {items.current.map((item: GoodSku, index: number) => {
  31. ? ? ? ? return (
  32. ? ? ? ? ? <InputNumber
  33. ? ? ? ? ? ? key={item.skuId}
  34. ? ? ? ? ? ? inputDisable
  35. ? ? ? ? ? ? min={0}
  36. ? ? ? ? ? ? max={50}
  37. ? ? ? ? ? ? value={item.num}
  38. ? ? ? ? ? ? onChange={v => {
  39. ? ? ? ? ? ? ? console.log(v, items)
  40. ? ? ? ? ? ? }}
  41. ? ? ? ? ? />
  42. ? ? ? ? )
  43. ? ? ? })}
  44. ? ? </View>
  45. ? )
  46. }
  47.  
  48. export default CartBrand

这样再打印的时候我们会看到

items中的selected已经变成true了
但是此时如果我们需要根据items中的selected去渲染不同的文字,会发现并没有变化。

  1. ? return (
  2. ? ? <View className={styles.brandBox}>
  3. ? ? ? {items.current.map((item: GoodSku, index: number) => {
  4. ? ? ? ? return (
  5. ? ? ? ? ? <View key={item.skuId}>
  6. ? ? ? ? ? ? <View>{item.selected ? '选中' : '未选中'}</View>
  7. ? ? ? ? ? ? <InputNumber
  8. ? ? ? ? ? ? ? inputDisable
  9. ? ? ? ? ? ? ? // 最小购买数量
  10. ? ? ? ? ? ? ? min={0}
  11. ? ? ? ? ? ? ? max={50}
  12. ? ? ? ? ? ? ? value={item.num}
  13. ? ? ? ? ? ? ? onChange={() => {
  14. ? ? ? ? ? ? ? ? console.log('selected', items)
  15. ? ? ? ? ? ? ? }}
  16. ? ? ? ? ? ? />
  17. ? ? ? ? ? </View>
  18. ? ? ? ? )
  19. ? ? ? })}
  20. ? ? </View>
  21. ? )

显示还是未选中

这是因为useRef的值会更新,但不会更新他们的 UI,除非组件重新渲染。因此我们可以手动更新一个值去强制让组件在我们需要的时候重新渲染。

  1. const CartBrand: FC<Props> = (props: Props) => {
  2. ? const { info } = props
  3. ? // 定义一个state,它在每次调用的时候都会让组件重新渲染
  4. ? const [, setForceUpdate] = useState(Date.now())
  5. ? const items = useRef<any>(
  6. ? ? info.items.map(item => {
  7. ? ? ? return { num: 1, selected: false }
  8. ? ? })
  9. ? )
  10. ? useEffect(() => {
  11. ? ? getCartStatus()
  12. ? }, [])
  13.  
  14. const getCartStatus = () => {
  15. ? ? setTimeout(() => {
  16. ? ? ? items.current = info.items.map(() => {
  17. ? ? ? ? return { num: 1, selected: true }
  18. ? ? ? })
  19. ? ? ? setForceUpdate()
  20. ? ? }, 5000)
  21. ? }
  22.  
  23. ? return (
  24. ? ? <View className={styles.brandBox}>
  25. ? ? ? {items.current.map((item: GoodSku, index: number) => {
  26. ? ? ? ? return (
  27. ? ? ? ? ? <View key={item.skuId}>
  28. ? ? ? ? ? ? <View>{item.selected ? '选中' : '未选中'}</View>
  29. ? ? ? ? ? ? <InputNumber
  30. ? ? ? ? ? ? ? inputDisable
  31. ? ? ? ? ? ? ? // 最小购买数量
  32. ? ? ? ? ? ? ? min={0}
  33. ? ? ? ? ? ? ? max={50}
  34. ? ? ? ? ? ? ? value={item.num}
  35. ? ? ? ? ? ? ? onChange={() => {
  36. ? ? ? ? ? ? ? ? console.log('selected', items)
  37. ? ? ? ? ? ? ? }}
  38. ? ? ? ? ? ? />
  39. ? ? ? ? ? </View>
  40. ? ? ? ? )
  41. ? ? ? })}
  42. ? ? </View>
  43. ? )
  44. }
  45.  
  46. export default CartBrand

这样我们就可以使用最新的items,并保证items相关的渲染不会出错

方案2. 使用useCallback

在InputNumber这个组件中,memo的第二个参数,我没有判断onClick回调是否相同,因为无论如何它都是不同的。
参考这个文章:use react memo wisely
函数对象只等于它自己。让我们通过比较一些函数来看看:

  1. function sumFactory() {
  2.  
  3. return (a, b) => a + b;
  4.  
  5. }
  6.  
  7. const sum1 = sumFactory();
  8.  
  9. const sum2 = sumFactory();
  10.  
  11. console.log(sum1 === sum2); // => false
  12.  
  13. console.log(sum1 === sum1); // => true
  14.  
  15. console.log(sum2 === sum2); // => true

sumFactory()是一个工厂函数。它返回对 2 个数字求和的函数。
函数sum1和sum2由工厂创建。这两个函数对数字求和。但是,sum1和sum2是不同的函数对象(sum1 === sum2is false)。
每次父组件为其子组件定义回调时,它都会创建新的函数实例。在自定义比较函数中过滤掉onClick固然可以规避掉这种问题,但是这也会导致我们上述的问题,在前面提到的文章中,为我们提供了另一种解决思路,我们可以使用useCallback来缓存回调函数:

  1. type Props = {
  2. ? info: any
  3. ? onUpdate: (items) => void
  4. }
  5.  
  6. const CartBrand: FC<Props> = (props: Props) => {
  7. ? const { info } = props
  8. ? const [items, setItems] = useState(
  9. ? ? info.items.map(item => {
  10. ? ? ? return { num: 1, selected: false }
  11. ? ? })
  12. ? )
  13. ? useEffect(() => {
  14. ? ? getCartStatus()
  15. ? }, [])
  16. ? // 获取当前购物车中所有的商品的库存状态
  17. ? const getCartStatus = () => {
  18. ? ? setTimeout(() => {
  19. ? ? ? setItems(
  20. ? ? ? ? info.items.map(() => {
  21. ? ? ? ? ? return { num: 1, selected: true }
  22. ? ? ? ? })
  23. ? ? ? )
  24. ? ? }, 5000)
  25. ? }
  26.  
  27. ? // 使用useCallback缓存回调函数
  28. ? const logChange = useCallback(
  29. ? ? v => {
  30. ? ? ? console.log('selected', items)
  31. ? ? },
  32. ? ? [items]
  33. ? )
  34.  
  35. ? return (
  36. ? ? <View className={styles.brandBox}>
  37. ? ? ? {items.map((item: GoodSku, index: number) => {
  38. ? ? ? ? return (
  39. ? ? ? ? ? <View key={item.skuId}>
  40. ? ? ? ? ? ? <InputNumber
  41. ? ? ? ? ? ? ? inputDisable
  42. ? ? ? ? ? ? ? // 最小购买数量
  43. ? ? ? ? ? ? ? min={0}
  44. ? ? ? ? ? ? ? max={50}
  45. ? ? ? ? ? ? ? value={item.num}
  46. ? ? ? ? ? ? ? onChange={logChange}
  47. ? ? ? ? ? ? />
  48. ? ? ? ? ? </View>
  49. ? ? ? ? )
  50. ? ? ? })}
  51. ? ? </View>
  52. ? )
  53. }

相应的,我们可以把InputNumber的自定义比较函数去掉。

  1. type Props = {
  2. ?inputDisable?: boolean
  3. ?// 是否一直展示输入框
  4. ?inputVisible?: boolean
  5. ?value: any
  6. ?min: number
  7. ?max: number
  8. ?onChange: (v: number) => void
  9. }
  10.  
  11. const InputNumber: FC<Props> = memo(
  12. ?(props: Props) => {
  13. ? ?const { inputDisable, max, min, value, inputVisible } = props
  14.  
  15. ? ?const handleUpdate = (e: any, num) => {
  16. ? ? ?e.stopPropagation()
  17. ? ? ?props.onChange(num)
  18. ? ?}
  19. ? ?return (
  20. ? ? ?<View className={styles.inputNumer}>
  21. ? ? ? ?{(value !== 0 || inputVisible) && (
  22. ? ? ? ? ?<>
  23. ? ? ? ? ? ?<Image
  24. ? ? ? ? ? ? ?className={styles.btn}
  25. ? ? ? ? ? ? ?src={require(value <= min
  26. ? ? ? ? ? ? ? ?? '../../assets/images/reduce-no.png'
  27. ? ? ? ? ? ? ? ?: '../../assets/images/reduce.png')}
  28. ? ? ? ? ? ? ?onClick={e => handleUpdate(e, value - 1)}
  29. ? ? ? ? ? ? ?mode='aspectFill'
  30. ? ? ? ? ? ?/>
  31. ? ? ? ? ? ?<Input
  32. ? ? ? ? ? ? ?value={value}
  33. ? ? ? ? ? ? ?disabled={inputDisable}
  34. ? ? ? ? ? ? ?alwaysEmbed
  35. ? ? ? ? ? ? ?type='number'
  36. ? ? ? ? ? ? ?cursor={-1}
  37. ? ? ? ? ? ? ?onInput={e => handleUpdate(e, parseInt(e.detail.value ? e.detail.value : '0'), 'input')}
  38. ? ? ? ? ? ?/>
  39. ? ? ? ? ?</>
  40. ? ? ? ?)}
  41. ? ? ? ?<Image
  42. ? ? ? ? ?className={styles.btn}
  43. ? ? ? ? ?src={require(max !== -1 && (value >= max || min > max)
  44. ? ? ? ? ? ?? '../../assets/images/plus-no.png'
  45. ? ? ? ? ? ?: '../../assets/images/plus.png')}
  46. ? ? ? ? ?onClick={e => handleUpdate(e, value + 1)}
  47. ? ? ? ?/>
  48. ? ? ?</View>
  49. ? ?)
  50. ?}
  51. )
  52.  
  53. export default InputNumber

这样在items更新的时候,inputNumber也会刷新,不过在复杂的逻辑中,比如items的结构非常复杂,items中很多字段都会有高频率的改变,那这种方式会减弱InputNumber中memo的效果,因为它会随着items的改变而刷新。

总结

在最后,我还是选择了方案一解决这个问题。同时提醒自己,memo的使用要谨慎??

到此这篇关于记一个React.memo引起的bug的文章就介绍到这了,更多相关React memo bug内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号