在 Class Component 中我们常常把函数绑在this上,保持其的唯一引用,以减少子组件不必要的重渲染。
class App {
constructor() {
// 方法一
this.onClick = this.onClick.bind(this)
}
onClick() {
console.log('I am `onClick`')
}
// 方法二
onChange = () => {}
render() {
return (
<Sub onClick={this.onClick} onChange={this.onChange} />
)
}
}
在 Function Component 中对应的方案即 useCallback :
// ✅ 有效优化
function App() {
const onClick = useCallback(() => {
console.log('I am `onClick`')
}, [])
return (<Sub onClick={onClick} />)
}
// ❌ 错误示范,`onClick` 在每次 Render 中都是全新的,<Sub> 会因此重渲染
function App() {
// ... some states
const onClick = () => {
console.log('I am `onClick`')
}
return (<Sub onClick={onClick} />)
}
useCallback可以在多次重渲染中仍然保持函数的引用, 第2行的onClick也始终是同一个,从而避免了子组件
<Sub>
的重渲染。
无限套娃✓[2]
相比较未使用useCallback带来的性能问题,真正麻烦的是useCallback带来的引用依赖问题。
// 当你决定引入 `useCallback` 来解决重复渲染问题
function App() {
// 请求 A 所需要的参数
const [a1, setA1] = useState('')
const [a2, setA2] = useState('')
// 请求 B 所需要的参数
const [b1, setB1] = useState('')
const [b2, setB2] = useState('')
// 请求 A,并处理返回结果
const reqA = useCallback(() => {
requestA(a1, a2)
}, [a1, a2])
// 请求 A、B,并处理返回结果
const reqB = useCallback(() => {
reqA() // `reqA`的引用始终是最开始的那个,
requestB(b1, b2) // 当`a1`,`a2`变化后`reqB`中的`reqA`其实是过时的。
}, [b1, b2]) // 当然,把`reqA`加到`reqB`的依赖数组里不就好了?
// 但你在调用`reqA`这个函数的时候,
// 你怎么知道「应该」要加到依赖数组里呢?
return (
<>
<Comp onClick={reqA}></Comp>
<Comp onClick={reqB}></Comp>
</>
)
}
从上面示例可以看到,当useCallback之前存在依赖关系时,它们的引用维护也变得复杂。调用某个函数时要小心翼翼,你需要考虑它有没有引用过时的问题,如有遗漏又没有将其加入依赖数组,就会产生
Bug。