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

参考

相关推荐
永乐春秋1 小时前
WEB攻防-通用漏洞&文件上传&js验证&mime&user.ini&语言特性
前端
鸽鸽程序猿1 小时前
【前端】CSS
前端·css
ggdpzhk1 小时前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js
学不会•3 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
工业甲酰苯胺5 小时前
Redis性能优化的18招
数据库·redis·性能优化
活宝小娜5 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点5 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow5 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o5 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic6 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端