一个bug引起的react运行机制原理性思考

文章讨论了在React中创建一个imglazyload高阶组件时遇到的问题,即如何获取组件内的img对象。通过使用div包裹组件并获取其ref,然后在useEffect中处理img对象,解决了在某些环境下懒加载功能失效的问题。问题的关键在于useEffect的执行顺序和ref的赋值时机。交换两个useEffect的顺序使得在正确的时间获取到了img对象,从而实现了懒加载功能。
摘要由CSDN通过智能技术生成

问题描述

出于封装img lazyload 高阶组件的需求导致的问题。由于需要获取到组件的img对象,所以需要获取到传过来的组件的ref,但是在高阶组件中需要通过ref转发来实现,相对复杂难理解故而放弃。最终使用在组件外包装一层div,获取此divdom对象,来达到获取内部组件的img对象。具体实现如下:

export default function withLazyLoad(WrappedComponent: any) {

  // 懒加载图片
  function lazyloadImg(node: Element | null, src: string) {
   // ......
  }

  return (props: IWithLazyLoad) => {
    const {
      lazyload = true,
      src,
      loading = '',
      error,
      ...compProps } = props

    const baseComponentRef = React.createRef<HTMLDivElement>();
    // 用于存储img对象
    const imgRef = useRef<any>(null)

    // 数据存在则触发懒加载
    useEffect(() => {
        if (lazyload && src && imgRef.current) {
          lazyloadImg(imgRef.current, src)
        } 
    }, [lazyload, src, imgRef.current])

    // 获取img对象
    useEffect(() => {
      if (baseComponentRef.current) {
          imgRef.current = baseComponentRef.current.querySelector('img')
      }  
    }, [baseComponentRef.current])

    return (
      <div ref={baseComponentRef} className={wrapperClassName} title={placeholder}>
        <WrappedComponent  {...compProps} src={loading} />
      </div>
    )
  }
}

问题表现

始终无法触发lazyloadImg方法,导致懒加载功能失效。
滚动后可以表现正常。
本地开发时表现正常,发布到dev环境就出问题(本地开发会触发 组件卸载-挂载两个过程)
原因分析
通过debugger可以发现,每次进行是否触发懒加载函数的条件判断时,imgRef.current的值始终是null,导致未曾进入if分支。
解决办法
将两个useEffect交换顺序即可解决,很邪门是不是?那且听我给你慢慢分析。

三个问题思考👀

1. 为什么交换了顺序就能解决imgRef.current取值始终为null的问题?

为了解答这个问题,我们先来了解一下三个知识点:

  • react的函数组件的加载机制
  • uesEffect的触发机制
  • useRef与createRef的区别

react的函数组件的加载机制

大家可以参考一下生命周期React生命周期详解
我简单阐述一下在函数组件中的加载过程(我的理解)
按照函数运行一样自上而下依次执行对应的语句,初始化state =》 初始化方法 =》按照出现顺序依次将 useEffect任务放到异步宏等待队列中 =》 渲染页面 =》 依次根据依赖执行useEffect方法定义的回调函数。如果回调中更改了state的值则重新进入这个加载流程,从而使视图更新。

辅助资料:

《React函数组件-useState原理》
React生命周期详解

useEffect的触发机制

常规上认为useEffect的依赖发生变化时,useEffect里的回调就会被调用。但是实际是因为,常规情况下useEffect的第二个参数中,传递的都是能够引起视图更新的变量。变量更改,视图更新,useEffect进行重新加载,对应的任务重新放置到异步执行队列中,从而产生 依赖的变量更新后,uesEffect回调被调用的效果。
useEffect 的清除函数在每次重新渲染时都会执行,而不是只在卸载组件的时候执行

useRef与createRef的区别

  • useRef只能在hook里使用,在后续组件重新渲染中保持引用值不变。用于获取元素的dom值时,是在组件渲染完成时才赋值,因此useEffect可能检测不到它的变更。想要实现ref转发的话需要结合forwardRef来实现。
  • createRef常用于类组件中,也可在hook组件中使用。在组件重新渲染时,引用值在不停的发生变化。

2. 为什么滚动页面就能触发?

页面滚动时,组件进行了重新渲染,在上一次渲染时已经对refcurrent赋值。再次触发useEffect时,就可以正常调用懒加载方法,从而生效。

3.为什么经历组件卸载-挂载两个过程就能正常表现?卸载过程做了什么?

在两个副作用函数中进行打印,打印结果告知

 // 数据存在则触发懒加载
   useEffect(() => {
     console.log('mount', imgRef.current)
     if (lazyload && src && imgRef.current) {
       lazyloadImg(imgRef.current, {})
     }

     return () => {
       console.log('unmount img', imgRef.current)
     }
   }, [lazyload, src, imgRef.current])

   // 获取img对象
   useEffect(() => {
     console.log('mount', baseComponentRef.current)
     if (baseComponentRef.current) {
       imgRef.current = baseComponentRef.current.querySelector('img')
     }
     return () => {
       console.log('unmount base', imgRef.current)
     }
   }, [baseComponentRef.current])

本地开发时,组件会经历挂载-卸载-挂载的过程,从而产生在第一次挂载时给imgRef设置了current的值,卸载时imgRef仍然保存着,所有在下一次正式挂载时,触发懒加载的依赖回调就会被执行,从而避免了线上未能触发懒加载的现象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值