开始
🌰一个真实的例子: 在组件进行挂载的时候需要监听页面的滚动事件,并且事件监听器中用到了state,这个state在另外一个useEffect函数中会被设置为页面上每个dom距离文档顶部的距离。那么在进行监听的时候,有如下函数
const initialTabTop = [0, 0, 0, 0, 0]
const [tapState, setTabTop] = useState(initialTabTop)
useEffect(() => {
...
setTabTop(getTapState(tabNum))
...
}, [])
const scrollHandler = function() {
console.log('scrollHandler', tapState)
// 下面代码用到了 tapState, 并且会调用一些set来改变当前组件的其他state
...
}
useEffect(() => {
window.addEventListener('scroll', scrollHandler)
return () => {
window.removeEventListener('scroll', scrollHandler)
}
}, [])
🏳️🌈请注意:
- 滚动事件的处理函数会改变当前组件的其他state,也就会导致组件的重新渲染。
- tapState除了初始挂载的时候,以及页面resize的时候外不会再变化
页面在滚动的时候,你会发现一直打印的都是initialTabTop,为什么?原因是你传入的依赖是一个空数组。
如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。
但是问题是,这里的effect中依赖的是一个函数,稍后请移步在依赖列表中省略函数是否安全?,为什么不安全?
原因是,你传入了空数组,那么这个effect中用到的函数scrollHandler就是react函数组件在第一次调用的时候,闭包中的那个
const [tapState, setTabTop] = useState(initialTabTop)得到的state,也就是这个initialTabTop了。
🛠️拍拍大腿,解决方案 在useEffect中添加这个scrollHandler函数作为依赖不就可以了
useEffect(() => {
window.addEventListener('scroll', scrollHandler)
return () => {
window.removeEventListener('scroll', scrollHandler)
}
}, [scrollHandler])
你觉得很好?并不是这样,由于scrollHandler函数会在页面滚动到某个位置的时候改变组件状态然后导致重新渲染,组件状态的变化会重新执行函数组件,由于scrollHandler函数是一个普通的函数,所以每次执行函数组件都会在这个闭包中创建一个新的scrollHandler,由于effect的依赖变了,所以effect会在重新渲染之后执行,因此结果是你每次滚动的时候,都执行了上面的effect。
🛠️挽救一下 思路是,每次滚动组件状态改变之后不会重新生成scrollHandler,因为scrollHandler只依赖于tapState,利用useCallback对scrollHandler补救一下:
const scrollHandler = useCallback(function() {
console.log('scrollHandler', tapState)
// 下面代码用到了 tapState, 并且会调用一些set来改变当前组件的其他state
...
}, [tapState])
useEffect(() => {
window.addEventListener('scroll', scrollHandler)
return () => {
window.removeEventListener('scroll', scrollHandler)
}
}, [scrollHandler])
这样就没什么毛病了。但是能不能优雅一点呢?
🛠️优雅一点
如果处于某些原因你 无法 把一个函数移动到 effect 内部,还有一些其他办法:
- 你可以尝试把那个函数移动到你的组件之外。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了。
- 如果你所调用的方法是一个纯计算,并且可以在渲染时调用,你可以 转而在 effect 之外调用它, 并让 effect 依赖于它的返回值。
- 万不得已的情况下,你可以 把函数加入 effect 的依赖但 把它的定义包裹 进 useCallback Hook。这就确保了它不随渲染而改变,除非 它自身 的依赖发生了改变。
首选是将一个函数移动到effect内部:
useEffect(() => {
const scrollHandler = useCallback(function() {
console.log('scrollHandler', tapState)
// 下面代码用到了 tapState, 并且会调用一些set来改变当前组件的其他state
...
}, [tapState])
window.addEventListener('scroll', scrollHandler)
return () => {
window.removeEventListener('scroll', scrollHandler)
}
}, [tapState])
注意了,set需要添加到依赖中,react的useState会保证,返回的set一直是稳定的。
🛠️其他方法呢
你可以尝试把那个函数移动到你的组件之外。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了。
这里不行,因为这个scrollHandler依赖了state以及其他set。
如果你所调用的方法是一个纯计算,并且可以在渲染时调用,你可以 转而在 effect 之外调用它, 并让 effect 依赖于它的返回值。
明显不行
万不得已的情况下,你可以 把函数加入 effect 的依赖但 把它的定义包裹 进 useCallback Hook。这就确保了它不随渲染而改变,除非 它自身 的依赖发生了改变。
这个就是拍拍大腿的解决方案了
🍀总结:最佳实践
如果处于某些原因你 无法 把一个函数移动到 effect 内部,还有一些其他办法:
- 你可以尝试把那个函数移动到你的组件之外。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了。
- 如果你所调用的方法是一个纯计算,并且可以在渲染时调用,你可以 转而在 effect 之外调用它, 并让 effect 依赖于它的返回值。
- 万不得已的情况下,你可以 把函数加入 effect 的依赖但 把它的定义包裹 进 useCallback Hook。这就确保了它不随渲染而改变,除非 它自身 的依赖发生了改变。
第三点存在就存在的理由:很明显可以复用这个useCallback返回的函数。