揭秘React的重新渲染机制

什么是重新渲染(re-render)

注意这里的渲染(render)其实是 react 中的协调(reconciliation) 过程,对比计算虚拟 DOM 树的差异,等到提交(commit)阶段更新到视图。

渲染分两种:初次渲染和重新渲染,什么情况下会触发组件的重新渲染,主要分为两种情况:

  • 组件自身状态的变化(useStateuseReduceruseContext)
  • 父级组件引起的重新渲染(props变化也属于这种情况)

props变化并不会引起子组件重新渲染,而根本原因就是当父组件渲染时,其所有的子组件都会重新渲染。

这里会有一个问题就是,一些情况下组件的重新渲染是没必要的,比如子组件并没有使用父组件的状态作为 props 或者 props 并没有更新,但父组件的重新渲染还是会导致子组件的重新渲染。

reconciliation 是从 root 开始,但会跳过所有父节点,到 state 发生变化的组件开始往下重新渲染。

不必要的重新渲染一般情况下不是问题,其性能开销并不大;在没有明显感知的卡顿情况下,可以不必进行优化。相反使用 React 提供的 memoization 相关的api造成的性能消耗可能会大于组件的重新渲染,得不偿失。

Don't optimize prematurely!(不要过早的性能优化)

但如果出现了性能问题,比如重新渲染触发频繁或者在性能开销大的组件上,则就必须用一些手段去跳过不必要的重新渲染。

举个例子,如Content组件内部有状态进行了更新,则Content会重新渲染,以及其所有的子组件会重新渲染。但可能 Tree 其实并不需要重新渲染,且这个组件渲染耗时较大,这时就需要想办法跳过 Tree 的 re-render。

text 复制代码
       App
      /   \
  Header  Content  ←----- update
          /     \
       Tree     Table

优化手段

这里使用dan的一个例子,ExpensiveTree是比较耗时的组件,当App组件修改颜色时,会导致App重新渲染,ExpensiveTree且没有依赖父组件的状态,是不要重新渲染的。

jsx 复制代码
import { useState } from 'react';

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

function ExpensiveTree() {
  const now = performance.now();
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>;
}

下面介绍一些优化手段:

使用memo

ExpensiveTree 使用memo函数包裹,会对props进行浅比较,如果有变化才更新。

jsx 复制代码
const ExpensiveTree = memo(() => {
  const now = performance.now();
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>;
});

注意使用memo是避免父组件引发的重新渲染,如果组件自身状态发生变化,则不受影响

类组件可以查看 PureComponent /shouldComponentUpdate

下放state

  • 一般情况下可通过封装组件解决,将变化的state封装到子组件,变成兄弟组件就不会相互影响了。找个方法就是要求对组件进行合理粒度的拆分和封装。
jsx 复制代码
export default function App() {
  return (
    <>
      <Form />
      <ExpensiveTree />
    </>
  );
}

function Form() {
  const [color, setColor] = useState("red");
  return (
    <>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
    </>
  );
}

const ExpensiveTree = memo(() => {
  ...
});

组件作为props

  • 将不需要渲染的组件作为 children 传递
jsx 复制代码
export default function App() {
  return (
    <ColorPicker>
      <ExpensiveTree />
    </ColorPicker>
  );
}

function ColorPicker({ children }) {
  let [color, setColor] = useState("red");
  return (
    <>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
       <p style={{ color }}>Hello, world!</p>
      {children}
    </>
  );
}

color变化时,ColorPicker会重新渲染,而ExpensiveTree作为App的子组件通过props.children进行传递, 引用并没有变化。

将组件作为props.xxx 传递也是一样的,children其实可以看作特殊的props

优化手段总结

组合

  • 下放state
  • 组件作为children/props传递

使用 memo

  • 如果props 是非基本类型的,需要配合使用useMemo/useCallback将传递的值进行缓存,防止引用变化

Context 相关

jsx 复制代码
function MyPage() {
  const [theme,setTheme] = useState('dark')
  return (
    <Context.Provider value={theme}>
       <Header />/
       <Content />
    </Context.Provider>
  );
}

注意,只有在Context.Provider 重新渲染的情况下,才会去对比value的变化,如果value的值发生变化,会使消费Context的相应组件重新渲染。

在上面的例子看,使用 state 作为value,通过 setter 触发,会使MyPage重新渲染,会导致所有子组件重新渲染,不论子组件是不是使用useContext。这里可以使用上文的优化措施,避免不必要的子组件渲染。

优化手段:

  • 如果不是在根组件使用Context.Provider,父组件可能导致其重新渲染的话,可使用useMemo记住value的值,防止value的引用变化
  • 进行Context的拆分,将不变的Context向外移,将变化的Context向内移动
  • 使用selectors,通过选择需要的状态从而规避掉无关的状态改变时带来的渲染开销。

参考

相关推荐
2301_80107415几秒前
TypeScript异常处理
前端·javascript·typescript
小阿飞_1 分钟前
报错合计-1
前端
caperxi3 分钟前
前端开发中的防抖与节流
前端·javascript·html
霸气小男3 分钟前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
susu10830189114 分钟前
前端css样式覆盖
前端·css
学习路上的小刘5 分钟前
vue h5 蓝牙连接 webBluetooth API
前端·javascript·vue.js
&白帝&6 分钟前
vue3常用的组件间通信
前端·javascript·vue.js
小白小白从不日白17 分钟前
react 组件通讯
前端·react.js
Redstone Monstrosity34 分钟前
字节二面
前端·面试
东方翱翔41 分钟前
CSS的三种基本选择器
前端·css