前言
- 在React框架下, 如果不做任何优化, 父组件渲染会触发子组件的重新render.
- 但由于协调算法的优化, 触发render不一定触发真实dom的更新.
- 但如果出现大量子组件重复render, 那么我们依旧可以考虑优化子组件渲染过程, 以获得可观的性能提升.
待优化示例
链接地址: https://codesandbox.io/s/hook-children-component-preformance1-es6bu
- 这是一个由多个小方格组成的棋盘, 点击小方格变为绿色, 再次点击变为灰色.
- 每次点击, 理论上只有当前点击的小方格需要更新, 但实际上, 其他的小方格也会重新render.
- 其他小方格重新render的原因是, 我们为了方便状态管理, 进行了状态提升, 将所有小方格的状态提升到了共有的父组件, 导致某些传入小方格的props的引用值发生了改变.
- 引用发生改变但引用的实际值可能并没有发生改变, 所以我们可以针对这一部分引用进行优化以达到优化组件重复渲染的目的.
解决思路
memo优化
const Block = memo((props) => {
const { handleBlockClick } = props
// 此处省略组件逻辑
return <div></div>;
});
- 使用memo包装Block组件, 但由于外部传入的handleBlockClick函数每次会重新生成, 所以Block组件依旧会重新render.
使用useCallback
const handleBlockClick = useCallback((blockIndex) => {
const newState = set(
state,
[blockIndex, "checked"],
!state[blockIndex].checked
);
setState(newState);
}, [])
- 我们可以尝试使用useCallback处理handleBlockClick, handleBlockClick函数不再发生更新, 所以也不会再触发其他Block组件的重新渲染.
-
但由于内部所依赖的闭包变量state也不再发生更新, 会导致每一次点击都是在初始化的state上进行更改, 所以点击多次后界面只会保留最后点击的绿色方块.
使用useRef
- 我们可以尝试使用useRef生成一个固定引用以挂载handleBlockClick函数, 这样handleBlockClick可以正常更新, 且useRef生成的引用并不会发生变更.
const handleBlockClick = (blockIndex) => {
const newState = set(
state,
[blockIndex, "checked"],
!state[blockIndex].checked
);
setState(newState);
}
// 这里请注意, 不能直接使用useRef(handleClick) 替代下面两行,
// 因为useRef中的参数是初始值, 并不会在每次执行useRef时更新ref.current.
const newRef = useRef()
newRef.current = handleBlockClick
// Block组件传入newRef
<Block
onClick={newRef}
/>
去掉current
- 使用useRef解决了Block组件重新渲染的问题, 并且也解决了useCallback造成的state无法更新的问题.
- 但Block组件内部需要以onClick.current()的形式调用外部传入的函数. 所以这里我们可以实现一个闭包函数以实现Block组件可以以onClick()的方式直接调用传入的函数, 去掉current后缀:
const newRefTarget = (...arr) => {
newRef.current.apply(null, arr)
}
- 然后使用useCallback处理闭包函数以确保newRefTarget不会重新生成.
const newRefTarget = useCallback(() => {
newRef.current.apply(null, arguments)
}, [])
<Block
onClick={newRefTarget}
/>
抽离逻辑为新的hooks
- 最后, 我们可以对之前优化handleBlockClick的逻辑进行抽离, 组装为一个新的hooks, 方便其他传入子组件的function复用:
const useFunction = (callback) => {
const newRef = useRef()
newRef.current = callback
return useCallback((...arr) => {
newRef.current.apply(null, arr)
}, [])
}
// Board组件中则使用useFunction生成的绑定索引的handleBlockClick
const handleBlockClick = useFunction((blockIndex) => {
const newState = set(
state,
[blockIndex, "checked"],
!state[blockIndex].checked
);
setState(newState);
})
<Block
onClick={handleBlockClick}
/>
优化后示例
链接: https://codesandbox.io/s/hook-children-component-preformance2-w0pj5
小结
- 使用memo使Block组件具备浅比较后优化渲染的特性
- 使用useRef创建固定索引挂载handleBlockClick函数
- 使用闭包函数优化useRef产生的current后缀问题
- 使用useCallback优化闭包函数使其保持不变
- 抽离优化逻辑为新的useFunction Hook