重新理解React re-render

前言

什么是re-render呢?re-render即重新渲染。那么在react中,哪些情况下会导致re-render呢?刚开始学习react的时候,我们大致会说以下情况下会导致re-render:

  • 组件内部的state变化;
  • 组件的props变化;
  • 用户交互事件触发或者接口请求数据响应;
  • 组件订阅的context发生变化;
  • 父组件的render

刚开始学习react的时候会错误的认为:当组件的state或props改变的时候,将从根节点开始,重新渲染更新整个组件树。这就是由于对re-render了解不够深刻而导致的错误认识。

1. 为什么要进行re-render

在我们页面第一次打开并加载显示,我们可以认为当前页面显示内容已经固定,犹如一张图片。当有接口请求响应、用户交互、页面自身程序执行等导致需要页面内容产生变化时候,就需要进行re-render来更新页面内容。

re-render并不意味着dom的变化,re-render是react自身执行的一系列组件自身生命周期函数执行、render、虚拟dom之间diff等阶段的执行,只有经过vdom之间的diff后,产生真正变化的dom才会更新真实dom,如果dom没有变化则不会更新dom。

2. 是什么情况导致re-render

上面我们有提到4种方式会导致re-render,但是他们都可以归结为:组件的state发生了变化。经过多次反复阅读官方文档,理解到react的渲染机制:如果一个组件内部的state发生了变化,那么react将重新渲染这个组件以及组件依赖的子组件。这个过程是递归进行的。这也解释了前面提到的问题,在react中,re-render是以发生state变化的当前组件开始向下递归进行re-render,而不是重新渲染整个组件树。

2.1 组件内部的state变化

组件内部的state变化,将会导致组件自身及子节点的re-render。当声明了一个state,无论在组件render中是否被使用,state变化都将导致组件自身及子节点re-render.

import { useState } from 'react';
import './App.scss';
import { ParentClass } from '@/components/yyy/ParentClass';

export default function App() {

  const [count, setCount] = useState(0);

  function addCount() {
    setCount(count + 1);
  }
  return (
    <div className="app">
      <h3>re-render</h3>
      <div>
        {/* count 没有被使用 */}
        {/* <p>当前值:{count}</p> */}
        <button onClick={addCount}>+1</button>
      </div>
      <ParentClass></ParentClass>
    </div>
  );
}
import React from 'react';

export class ParentClass extends React.Component {
  override componentDidMount(): void {
    console.log('parent did mount');
  }

  override componentDidUpdate(prevProps: Readonly<{}>, prevState: Readonly<{}>, snapshot?: any): void {
    console.log('parent did update');
  }

  override shouldComponentUpdate(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): boolean {
    console.log('parent should update');

    return true;
  }

  override render() {
    console.log('parent render');
    return <div>父节点</div>;
  }
}

上面两个组件App和ParentClass组件,App组件内部声明的state变量count并没有被使用,现在我们点击+1按钮,可以看到控制台同样打印出了ParentClass内部的结果。

重新理解React re-render

2.2 组件的props发生变化

组件的props发生变化,是我们站在子组件的角度得出的结论,而子组件的props发生变化,必然是父组件的state发生变化,从而导致传递给子组件的props发生变化,所以根本原因还是组件的state发生变化。

2.3 用户交互事件触发或者接口请求数据响应

有了上面的理解,这个自然可以理解为根本原因还是state状态发生变化,从而导致re-render。用户交互事件后者接口响应数据改变了state,从而导致re-render.

2.4 组件订阅的context发生变化

组件订阅的context的value发生变化,是由于provider中的state变化导致,同样是state发生变化导致的re-render

2.5 父组件的render

父组件的更新,不论子组件的props是否发生变化,都将导致子组件re-render,除非组件是一个纯组件(纯组件:即如果他的props没有发生变化,那么它就不会更新渲染。)可以使用React.memo()、React.PureComponent将组件变为纯组件。

经过前面几个方面的分析,我们可以得出大致结论:是由于组件的state发生变化从而导致re-render。

3. 如何避免不必要的re-render

首先看下一段代码:

import { useState } from 'react';
import './App.scss';
import { ParentClass } from '@/components/yyy/ParentClass';

export default function App() {
  console.log('app render');
  const [count, setCount] = useState(0);
  function addCount() {
    setCount(count + 1);
  }
  return (
    <div className="app">
      <h3>re-render</h3>
      <div>
        <p>app count值:{count}</p>
        <button onClick={addCount}>app+1</button>
      </div>
      <ParentClass></ParentClass>
    </div>
  );
}

import React from 'react';

import { ChildClass } from './ChildClass';

interface IProps {}

interface IState {
  count: number;
}

export class ParentClass extends React.Component<IProps, IState> {
  constructor(props: IProps) {
    super(props);
    this.state = {
      count: 0
    };

    this.addCount = this.addCount.bind(this);
  }

  override componentDidMount(): void {
    console.log('parent did mount');
  }

  override componentDidUpdate(prevProps: Readonly<{}>, prevState: Readonly<{}>, snapshot?: any): void {
    console.log('parent did update');
  }

  override shouldComponentUpdate(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): boolean {
    console.log('parent should update');

    return true;
  }

  addCount() {
    this.setState((state) => ({ count: state.count + 1 }));
  }

  override render() {
    console.log('parent render');
    return (
      <div>
        <h3>parent class node</h3>
        <p>parent count值:{this.state.count}</p>
        <button onClick={this.addCount}>parent count+1</button>
        <ChildClass></ChildClass>
      </div>
    );
  }
}

import React from 'react';

export class ChildClass extends React.Component {
  override componentDidMount(): void {
    console.log('child did mount');
  }

  override componentDidUpdate(prevProps: Readonly<{}>, prevState: Readonly<{}>, snapshot?: any): void {
    console.log('child did update');
  }

  override shouldComponentUpdate(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): boolean {
    console.log('child should update');

    return true;
  }

  override render() {
    console.log('child render');
    return (
      <div>
        <h3>child class node</h3>
      </div>
    );
  }
}

在上面的例子中,app为根节点,parentClass为父节点,childClass为子节点,点击页面上的app+1按钮或者parent+1按钮,都会导致childClass的重新渲染。而假如childClass节点内部处理的逻辑比较复杂,更新比较耗时,这将会导致不必要的更新渲染。

3.1 状态下放

由于react的更新渲染机制,组件自身的state变化,将导致组件自身及依赖的子组件更新。所以我们可以尽量将状态state下放,减少不必要组件的更新。

观察上面的parentClass代码

<div>
        <h3>parent class node</h3>
        <p>parent count值:{this.state.count}</p>
        <button onClick={this.addCount}>parent count+1</button>
        <ChildClass></ChildClass>
</div>

只有两处用到count状态,我们可以将以下两处单独抽离到一个组件count

<div>
        <h3>parent class node</h3>
        <Count></Count>
        <ChildClass></ChildClass>
</div>

这样,count的改变将不会引起childClass的不必要更新。

3.2 组合

我们可以重新组合parent组件,将childClass组件从parentClass组件中提取出来,避免由于parentClass状态的改变导致childClass的不必要更新。

3.3 React.memo 或 React.PureComponent

React.memo

React.memo()是一个高阶组件,在16.6.0版本中加入的新组件。用于函数组件的包装缓存,避免函数组件不必要的渲染。它通过对前后的props进行浅比较,如果前后props不一致,改组件将重新渲染,反之则不进行渲染。

用法:

import React from 'react';

function ChildFn() {
  console.log('memo child render');
  return (
    <div>
      <h3>这是一个函数子组件</h3>
      <p>通过使用React.memo()来对改组件进行渲染优化</p>
    </div>
  );
}

export const MemoChildFn = React.memo(ChildFn);

React.memo的使用注意:

  1. 由于React.memo()采用浅比较来对比前后的props是否改变;所以当我们的props为引用型数据类型,在值变化的同时我们需要给props一个新的引用值。
import React from 'react';

interface IProps {
  list: Array<string>;
}

function ChildFn(props: IProps) {
  console.log('memo child render');
  return (
    <div>
      <h3>这是一个函数子组件</h3>
      <p>通过使用React.memo()来对改组件进行渲染优化</p>
      <ul>
        {props.list.map((item, index) => {
          return <li key={index}>{item}</li>;
        })}
      </ul>
    </div>
  );
}

export const MemoChildFn = React.memo(ChildFn);
import { useState } from 'react';
import './App.scss';
import { MemoChildFn } from '@/components/yyy/ChildFn';

export default function App() {
  const [list, setList] = useState(['li', 'zhang', 'kong']);
  const addTom = () => {
    // props没有改变,不会更新
    list.push('tom');
    setList(list);
    // 正确更新方式
    const newList = [...list, 'tom']
    setList(newList);
  };
  return (
    <div className="app">
      <h3>re-render</h3>
      <button onClick={addTom}>添加tom</button>
      <MemoChildFn list={list}></MemoChildFn>
    </div>
  );
}

  1. 既然memo很好用,那么是不是可以对所有的函数组件都进行包裹一下呢?

答案是否定的,

  • 不要过早优化,因为缓存也是有成本的。
  • memo会浅比较props的每一个值,这也是一种计算消耗。

React.PureComponent

对于类组件不必要的更新渲染,我们可以自定义类组件的shouldComponentUpdate来决定何时进行更新渲染,也可以直接使用官方自带shouldComponentUpdate的React.PureComponent类组件。

React在v15.5的时候引入了Pure Component组件,React在进行组件更新时,如果发现这个组件是一个PureComponent,它会将组件现在的state和props和其下一个state和props进行浅比较,如果它们的值没有变化,就不会进行更新。

为了防止组件因为父组件的重新渲染而重新渲染,可以将其声明为PureComponent的子类,PureComponent开箱即用地实现了shouldComponentUpdate。如果PureComponent的子类组件定义了shouldComponentUpdate,则以自定义为主。

4. 总结

理解react的更新渲染流程以及如何避免不必要的更新渲染是我们开发中非常重要的一个环节,文章如有理解不对之处欢迎评论交流学习。

原文链接:https://juejin.cn/post/7257441061873696829 作者:ktb07

(0)
上一篇 2023年7月20日 上午10:26
下一篇 2023年7月20日 上午10:36

相关推荐

发表回复

登录后才能评论