现象
在页面中事件触发回调时,偶现内层函数没有被调用。打印后发现,内层函数在调用时打印为空,但是在组件中打印有值。大概如下:
// 最内层被调用
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 函数一直为最新的更新。
react 组件在执行时,往往由于传入参数更改,函数会执行好多次。这个函数也是由于这个特性,持续的更新fnRef,在remainRn中引用fnRef可以一直是最新的函数,但是返回的remainFn却始终是同一个。
在上面的场景中,由于renmainFn不变更,上面的 Test1 组件由于传入函数没有变更,导致外面传入的 console.log 即使变更,外面 innerFoo2 也不会跟随变更,被调用的 Test1 也不会更新,调用的时候使用的也是旧的函数
再回到 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。这里单纯看还是很难能理解的,但是结合场景就比较好查了。