从一个 React Bug 浅谈React hooks 链表

54 阅读2分钟

现象

在页面中事件触发回调时,偶现内层函数没有被调用。打印后发现,内层函数在调用时打印为空,但是在组件中打印有值。大概如下:

// 最内层被调用
const Test1({fooCall}) {
    const innerFoo1 = useRemainFn(() => {
        setTimeout(() => {
            console.log(fooCall) // 有值
        }, 500);
    })

    useEffect(() => {
        innerFoo1()
    }, [innerFoo1])

    return <div></div>
}

// 外层调用
const Test2({outerFoo}) {
    console.log(outerFoo) // 有值

    const innerFoo2 = useRemainFn(() => {
        console.log(outerFoo) // 打印 undefined
    })

    return <Test1 fooCall={innerFoo2}></Test1>
}

// 最外层整体调用
return <Test2 outerFoo={console.log}></Test2>

useRemainFn 的定义如下:

export function useRemainFn(fn) {
  const fnRef = useRef(fn)
  fnRef.current = fn

  const remainFn = useRef<T>()

  if (!remainFn.current) {
    remainFn.current = function (...args) {
      return fnRef.current.apply(this, args)
    }
  }

  return remainFn.current
}

这个函数是用来保持一个函数,在深层调用时仍然可以保持当前函数及其依赖。函数本身是没有问题的,但是项目中出现了滥用情况,不管什么场景都包裹一下。

问题

根据行文也能看出来,其实问题就是出现在这 useRemainFn 这里了。首先分析一下这个函数做了什么。
函数在第一次执行的时候定义了两个 useRef hooks,使用 remainFn 包裹 fnRef 保存的执行函数来达到返回函数不变,而传入的 fn 函数一直为最新的更新。

image.png

react 组件在执行时,往往由于传入参数更改,函数会执行好多次。这个函数也是由于这个特性,持续的更新fnRef,在remainRn中引用fnRef可以一直是最新的函数,但是返回的remainFn却始终是同一个。
在上面的场景中,由于renmainFn不变更,上面的 Test1 组件由于传入函数没有变更,导致外面传入的 console.log 即使变更,外面 innerFoo2 也不会跟随变更,被调用的 Test1 也不会更新,调用的时候使用的也是旧的函数

image.png

再回到 useRemainFn这个函数。在刚开始看这个函数的时候,我认为使用 let/const 就可以替代 fnRef。但是定位到这个bug之后,才知道为什么需要如此定义。\

export function useRemainFn(fn) {
  const fnRef = {
      current: fn
  }

  const remainFn = useRef<T>()

  if (!remainFn.current) {
    remainFn.current = function (...args) {
      return fnRef.current.apply(this, args)
    }
  }

  return remainFn.current
}

如果这里的代码这样写,其实在后续的更新中,remainFn 由于已经有值,不会更新,导致使用的是第一次传入的 fn。这里单纯看还是很难能理解的,但是结合场景就比较好查了。