React 组件 re-render 优化

React 中触发组件 re-render 3 种方式

  1. 组件使用 useState、useReducer 触发 re-render
  2. 组件使用 uesContext,由 context 触发 re-render
  3. 父组件更新,触发子组件 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 />
    </>
  )
}
1
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 />
    </>
  )
}
1
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 />
    </>
  )
}
1
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>
1
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} />
    </>
  )
}
1
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')
}, [])
1
2
3

如果 handler 中引用父组件的 count 数据

const handler = useCallback(function() {
  console.log('count', count)
  console.log('props handler')
}, [])
1
2
3
4

点击 Hello 组件中按钮时 count 值也被缓存,始终打印 0

这并非我们要期待的,handler 中要能访问到 count 最新值

将 count 作为 useCallback 依赖可以解决这个问题

const handler = useCallback(function() {
  console.log('count', count)
  console.log('props handler')
}, [count)
1
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} />
    </>
  )
}
1
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

这个方案整体策略是:

  1. useRef 持有 count 值,给 handler 使用
  2. useEffect 更新 useRef 持有 count 值
  3. useCallback 返回固定的 handler

# 小结

开发中常见的是父组件更新引起的子组件的不必要的更新

通常的解决方案

  1. 隔离父组件变化的部分
  2. 子组件使用 React.memo 优化
  3. 子组件通过插槽方式注入到父组件中

但对于父组件直接传递引用数据,比如函数给子组件引起的更新

以上的策略都将失效,父组件每次 re-render 都导致函数重新创建

子组件的 props 变化了必然引起 re-render

所以使用 useCallback 缓存函数

为了处理函数中访问数据的问题,需要结合 useRef、useEffect

扫一扫,微信中打开

微信二维码