React 中触发组件 re-render 3 种方式
- 组件使用 useState、useReducer 触发 re-render
- 组件使用 uesContext,由 context 触发 re-render
- 父组件更新,触发子组件 re-render(补动)
前两种属于组件正常更新
第 3 种的 re-render 很多时候并非是我们想要的结果
此时就要优化组件设计,保证父组件的更新不会意外导致子组件 re-render
# 测试代码
父组件 setCount
时触发子组件 Hello Render
这并非所希望发生的,因为子组件 Hello 并不需要 re-render
// 子组件,接收 handler 函数
function Hello({handler}) {
console.log('Hello render')
return(
<div>
<button onClick={handler}>使用父组件的 handler</button>
</div>
)
}
// 父组件
function Box() {
console.log('Box render')
const [count, setCount] = useState(0)
return(
<>
<h3>{count}</h3>
<button
onClick={() => setCount(count + 1)}
>setCount</button>
<Hello />
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 隔离父组件变化部分为独立组件
setCount
是变化部分,将此部分逻辑隔离
const Counter = () => {
const [count, setCount] = useState(0)
return(
<>
<h3>{count}</h3>
<button
onClick={() => setCount(count + 1)}
>setCount</button>
</>
)
}
// 此时 Counter 组件更新,不会触发 Hello 组件更新
function Box() {
return(
<>
<Counter />
<Hello />
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# React.memo 优化 Hello 组件
隔离变化部分,相对来说改动还是比较大的,有一定的测试成本
React.memo 优化 Hello 更新策略
function MemoHello = React.memo(Hello)
function Box() {
console.log('Box render')
const [count, setCount] = useState(0)
return(
<>
<h3>{count}</h3>
<button
onClick={() => setCount(count + 1)}
>setCount</button>
{/* Memo 处理后的 Hello 组件,不会由于父组件中 setCount 更新引起更新 */}
<MemoHello />
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 将 Hello 组件以插槽形式注入到父组件中渲染
父组件通过解构 props,得到 children,然后注入到对应的渲染位置
function Box({children}) {
console.log('Box render')
const [count, setCount] = useState(0)
return(
<>
<h3>{count}</h3>
<button
onClick={() => setCount(count + 1)}
>setCount</button>
{/* 插槽形式注入 */}
{children}
</>
)
}
<Box>
<Hello />
</Box>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 父组件传递给子组件 props 导致的意外更新
父组件通过 props 传递给子组件数据
如果传递的引用数据,比如函数
不对函数进行缓存会引起子组件非必要更新
function Box() {
console.log('Box render')
const [count, setCount] = useState(0)
const handler = function() {
console.log('props handler')
}
return(
<>
<h3>{count}</h3>
<button
onClick={() => setCount(count + 1)}
>setCount</button>
<MemoHello handler={handler} />
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
handler 在父组件每次 setCount 时都会重新创建
就算 Hello 组件使用 React.memo 进行了优化
而缓存策略基于 Object.is
浅比较
每次更新,新旧 handler 引用不同,导致 Hello 组件 re-render
# 所以使用 useCallback 缓存下 handler 函数
const handler = useCallback(function() {
console.log('props handler')
}, [])
2
3
如果 handler 中引用父组件的 count 数据
const handler = useCallback(function() {
console.log('count', count)
console.log('props handler')
}, [])
2
3
4
点击 Hello 组件中按钮时 count 值也被缓存,始终打印 0
这并非我们要期待的,handler 中要能访问到 count 最新值
将 count 作为 useCallback 依赖可以解决这个问题
const handler = useCallback(function() {
console.log('count', count)
console.log('props handler')
}, [count)
2
3
4
但 handler 函数由于 count 变化就会重新创建,于是触发 Hello 组件 re-render
# useRef + useEffect 解决 handler 重新创建、count 最新值问题
function Box() {
console.log('Box render')
const [count, setCount] = useState(0)
// countRef 只初始化一次,re-render 不会重新创建
const countRef = useRef(count)
const handler = useCallback(function() {
// 读取 countRef 值
console.log('count', countRef.current)
console.log('props handler')
// 没有依赖
}, [])
// 保证 countRef 持有最新 count 值
useEffect(() => {
countRef.current = count
}, [count])
return(
<>
<h3>{count}</h3>
<button
onClick={() => setCount(count + 1)}
>setCount</button>
{/* 不必使用 React.memo */}
<Hello handler={handler} />
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
这个方案整体策略是:
- useRef 持有 count 值,给 handler 使用
- useEffect 更新 useRef 持有 count 值
- useCallback 返回固定的 handler
# 小结
开发中常见的是父组件更新引起的子组件的不必要的更新
通常的解决方案
- 隔离父组件变化的部分
- 子组件使用 React.memo 优化
- 子组件通过插槽方式注入到父组件中
但对于父组件直接传递引用数据,比如函数给子组件引起的更新
以上的策略都将失效,父组件每次 re-render 都导致函数重新创建
子组件的 props 变化了必然引起 re-render
所以使用 useCallback 缓存函数
为了处理函数中访问数据的问题,需要结合 useRef、useEffect
扫一扫,微信中打开