React 里引起 rernder 的几种情况

React 组件的生命周期大概是这样:

mount -> init-render -> re-render -> unmount

在线demo。可对照着查看。

由于很久不用类组件了,以下都只针对函数式组件。

1. props 变动,引起的渲染

此处省略。

2. context 变动,引起的渲染

建议先通读一下官方文档 React Context

简单来说, context 是用于组件嵌套过深时,避免一层层传递 props ,传递参数用的。

使用 context , 需要注意的是:

3. 父组件渲染引起的子组件重新渲染

这是平常开发中比较容易忽略的一点,前端现在都是组件嵌套组件,形成一个巨大的组件树, React 只要确认了组件需要 render , 在其之下的所有子组件都会走一遍 render 。

在如今动则上百个组件的页面里,很容易就会引起某些子组件额外的 render ,造成性能浪费。

React 提供了两个工具来给开发者手动进行这方面的优化。函数组件的 React.memo , 类组件的 shouldComponentUpdate

4. 组件 unmount -> mount 引起的渲染

这个场景也不常见, unmount 后,再 mount ,触发渲染是理所应当的。所以问题不在触发了渲染,问题在于什么时候触发的 unmount 。

实际遇到的几个场景都写在demo里了。

  1. xx && <Comp> 这种写法比较常见,会触发组件的 unmount , 但不会马上紧接着 mount , 所以一般情况下是没啥问题的。
  2. 列表里长度不一样或者 key 不一样,导致的 unmount 。此外还要分情况,如果列表前后长度不一样了,会导致卸载或者新增的情况,这一般都是期望的行为。 但如果列表前后长度一致,其中某几个 key 变动了,但是组件的类型是一样的。 这时候对比前后两轮的 key 值,不存在的 key 对应的组件 unmount , 新增的 key 对应的组件 mount , 依旧存在的 key 复用组件实例,触发 render 。
  3. 组件的引用地址变动,导致 diff 时判定了 unmount,然而实际上也是同一个组件占了这个位置,继而马上 mount 。 这种情况比较特别,目前只发现一种情况下会出现,即组件内部声明组件。如下:
export const UnmountMountPaper3 = () => {
  const forceUpdate = useForceRender();
  useLogger('ParentNode');

  const InnerChild = () => {
    useLogger('InnerChild');
    const [num, setNum] = React.useState(0);

    return (
      <div>
        <div
          onClick={() => {
            forceUpdate();
            setNum((n) => n + 1);
          }}
        >
          click me : {num}
        </div>
        <Typography p={1} component="p">
          InnerChild
        </Typography>
      </div>
    );
  };

  return (
    <Paper sx={{ m: 1, width: '100%' }}>
      <InnerChild />
      <Button
        onClick={() => {
          forceUpdate();
        }}
      >
        触发组件更新
      </Button>
    </Paper>
  );
};

这种情况容易出问题,比如上面例子中的 click me 按钮 ,点击了应该是把 num + 1 才对,可实际不管点击多少次都是0。 这是因为 InnerChild 是 unmount -> mount -> render 这样一个过程。因为走了一遍 mount 过程, num 的值又被初始化为 0 了。

5. state 变动,引起的渲染

抛开 Class 组件的 forceUpdate 方法,其实某种意义上,可以说 React 里能触发渲染的就只有一种方式,那就是改变 state 。 上文中,除了 mount 引起的渲染,就剩下 props 和 context 引起的渲染了,可这两者针对的都是组件,这两者的修改都不会导致 React 的渲染。 比如一个组件,你传给 props 或者 context 的 provider 一个任意值,然后你修改这个值,是不会有任何变化的。 因为 React 不是响应式的,你修改了值,你想把结果渲染到页面上,你就需要通知 React 走一次 render 过程,而这个通知的方法就是改变 state 。

这也是个人比较喜欢 React 的原因,简洁,不复杂,纯粹。

  1. setState 会导致渲染,这个每个人都能理解。
  2. 因为自定义hook的出现,出现了很多不是那么直接的 setState
  1. Redux 触发更新的方式。

以下摘自 react-redux 7.x 版本的代码:

Connect 最终触发 React 更新的代码

function storeStateUpdatesReducer(state, action) {
  const [, updateCount] = state
  return [action.payload, updateCount + 1]
}

useSelector 最终触发 React 更新的代码

const [, forceRender] = useReducer((s) => s + 1, 0)

由上面代码可以看出来, redux 最终通知 React 进行一次渲染,就是通过state的改变来做的。

  1. Mobx 触发更新的方式

以下摘自 mobx-react-lite 的代码:

const [, setState] = React.useState()
const forceUpdate = () => setState([] as any)
使用 Discussions 讨论 Github 上编辑