温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

如何解决React.memo引起的bug问题

发布时间:2022-03-08 13:45:44 来源:亿速云 阅读:197 作者:小新 栏目:开发技术

这篇文章主要介绍如何解决React.memo引起的bug问题,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!

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

    问题描述

    我们在处理业务需求时,会用到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中更新数据,会看到:

    如何解决React.memo引起的bug问题

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

    如何解决React.memo引起的bug问题

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

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

    如何解决React.memo引起的bug问题

    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>
      )

    显示还是未选中

    如何解决React.memo引起的bug问题

    这是因为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相关的渲染不会出错

    方案2. 使用useCallback

    在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问题”这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注亿速云行业资讯频道!

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    AI