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

参考

相关推荐
挣扎与觉醒中的技术人6 分钟前
【技术干货】三大常见网络攻击类型详解:DDoS/XSS/中间人攻击,原理、危害及防御方案
前端·网络·ddos·xss
zeijiershuai10 分钟前
Vue框架
前端·javascript·vue.js
写完这行代码打球去12 分钟前
没有与此调用匹配的重载
前端·javascript·vue.js
华科云商xiao徐12 分钟前
使用CPR库编写的爬虫程序
前端
狂炫一碗大米饭15 分钟前
Event Loop事件循环机制,那是什么事件?又是怎么循环呢?
前端·javascript·面试
IT、木易16 分钟前
大白话Vue Router 中路由守卫(全局守卫、路由独享守卫、组件内守卫)的种类及应用场景
前端·javascript·vue.js
顾林海17 分钟前
JavaScript 变量与常量全面解析
前端·javascript
程序员小续17 分钟前
React 组件库:跨版本兼容的解决方案!
前端·react.js·面试
乐坏小陈18 分钟前
2025 年你希望用到的现代 JavaScript 模式 【转载】
前端·javascript
生在地上要上天19 分钟前
从600行"状态地狱"到可维护策略模式:一次列表操作限制重构实践
前端