揭秘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,通过选择需要的状态从而规避掉无关的状态改变时带来的渲染开销。

参考

相关推荐
雾恋3 小时前
最近一年的感悟
前端·javascript·程序员
A黄俊辉A4 小时前
axios+ts封装
开发语言·前端·javascript
小李小李不讲道理4 小时前
「Ant Design 组件库探索」四:Input组件
前端·javascript·react.js
连合机器人5 小时前
晨曦中的守望者:当科技为景区赋予温度
java·前端·科技
郑板桥305 小时前
tua-body-scroll-lock踩坑记录
前端·javascript
慢半拍iii6 小时前
JAVA Web —— A / 网页开发基础
前端
gnip6 小时前
pnpm 的 monorepo架构多包管理
前端·javascript
新手村领路人8 小时前
Firefox自定义备忘
前端·firefox
乖女子@@@8 小时前
css3新增-网格Grid布局
前端·css·css3
伐尘9 小时前
【CE】图形化CE游戏教程通关手册
前端·chrome·游戏·逆向