React 性能优化--无用渲染以及解决方法和Context上下文对象

454 阅读6分钟

无用渲染

组件会有自己本地的状态(state), 当它们的值由于用户的操作而发生改变时,组件就会重新渲染。在项目中,一个组件可能会被频繁地进行渲染,这些渲染虽然有一小部分是必须的,不过大多数都是无用的,它们的存在会大大降低我们应用的性能。

类组件的无用渲染解决方案

import React from "react";
import Des from "./des";
class App extends React.Component {
    render() {
        return (
            <div>
                <Des></Des>
            </div>

        )
    }
}
export default App;


import React, { Component } from 'react'

export default class Des extends Component {
  state={
    num:1
  }
  change=()=>{
    this.setState({num:20});
  }
  render() {
    console.log("render运行,页面重新渲染");
    return (
      <div>
        <p>{this.state.num}</p>
        <button onClick={this.change}>change</button>
      </div>
    )
  }
}

2.gif

以上代码中第一次修改了数据页面刷新,render函数再次运行。之后数据没有发生变化,页面没有重新渲染,但是再次调用this.setState使得render函数再次运行,这就是无用的渲染就使得性能降低。

类组件可以使用shouldComponentUpdate和PureComponent来解决无用的渲染。

shouldComponentUpdate生命周期函数

当React想要渲染一个组件的时候,它将会调用这个组件的shouldComponentUpdate钩子函数, 这个函数会告诉它是不是真的要渲染这个组件。返回false就不会重新渲染也就不会执行render函数,返回true就会重新渲染,默认返回true。可以自己实现逻辑决定是否让页面重新渲染。

shouldComponentUpdate(nextProps, nextState) {
}
其中各个参数的含义是:
nextProps: 组件将会接收的下一个参数props
nextState: 组件的下一个状态state
shouldComponentUpdate函数一直返回true,这就告诉React,无论何种情况都要重新渲染该组件。
这个方法的返回值是falseReact永远都不会重新渲染我们的组件。
import React, { Component } from 'react'

export default class Des extends Component {
  state={
    num:1
  }
  change=()=>{
    this.setState({num:20});
  }
  shouldComponentUpdate(nextprops,nextstate){
    if(nextstate.num==this.state.num){
      return false;
    }
    return true;
  }
  render() {
    console.log("render运行,页面重新渲染");
    return (
      <div>
        <p>{this.state.num}</p>
        <button onClick={this.change}>change</button>
      </div>
    )
  }
}

2.gif

PureComponent

React在进行组件更新时,如果发现这个组件是一个PureComponent,它会将组件现在的所有state和props和其下一个state和props进行比较,如果它们的值没有变化,就不会进行更新。要想让你的组件成为PureComponent,只需要继承React.PureComponent

import React, { PureComponent } from 'react'

export default class Des extends PureComponent {
  state={
    num:1
  }
  change=()=>{
    this.setState({num:20});
  }
  render() {
    console.log("render运行,页面重新渲染");
    return (
      <div>
        <p>{this.state.num}</p>
        <button onClick={this.change}>change</button>
      </div>
    )
  }
}

2.gif

在扩展React.PureComponent时不应该使用shouldComponentUpdate,两者使用其一即可。

区别

  • PureComponent会内置一个shouldComponentUpdate,通过 prop 和 state 的浅比较来实现,当 prop 或 state 是基本数据其值或者引用数据的引用地址发生改变时,组件就会发生更新。
  • shouldComponentUpdate可以处理引用类型的深层次比较,但也会影响效率。

函数组件无用渲染的解决方案

函数组件不存在继承关系,所以函数组件提供了React.memo,它其实就是函数组件的React.PureComponent,是用来控制函数组件是否要重新渲染的。

import React from "react";
import First from "./First";
class App extends React.Component {
    state={
        info:'父组件的数据'
    }
    change=()=>{
        this.state.info='父组件数据被修改,会导致render函数运行然后重新传值给子组件';
        this.setState(this.state);
    }
    render() {
        return (
            <div>
                <First data={this.state.info}></First>
                <button onClick={this.change}>change</button>
            </div>

        )
    }
}
export default App;

export default function First(props) {
  console.log('First组件被重新渲染');
  return (
    <div>
        <p>{props.data}</p>
    </div>
  )
}

子组件接受的属性并没有改变,页面并没有刷新,但是父组件执行了this.setState执行后子组件的render函数重新运行了。

2.gif

React.memo

React.memo会返回一个纯化的组件MemoFuncComponent,这个组件将会在JSX标记中渲染出来。当组件的参数props和状态state发生改变时,React将会检查前一个状态和参数是否和下一个状态和参数是否相同,如果相同,组件将不会被渲染,如果不同,组件将会被重新渲染。

只需要调用memo函数,将函数组件作为参数,再导出返回的组件即可。

import React from "react";
import First from "./First";
class App extends React.Component {
    state={
        info:'父组件的数据'
    }
    change=()=>{
        this.state.info='父组件数据被修改,会导致render函数运行然后重新传值给子组件';
        this.setState(this.state);
    }
    render() {
        console.log(1111);
        return (
            <div>
                <First data={this.state.info}></First>
                <button onClick={this.change}>change</button>
            </div>

        )
    }
}
export default App;


import {memo} from 'react'
 function First(props) {
  console.log('First组件被重新渲染');
  return (
    <div>
        <p>{props.data}</p>
    </div>
  )
}

export default memo(First);

父组件的render函数运行了,但是函数组件接受的属性并没有再改变,所以函数组件的render函数没有再运行。

React Context上下文对象

Context属于React的高级API,官方并不建议在稳定版的App中使用Context。很多优秀的React组件都通过Context来完成自己的功能,比如react-redux就是通过Context提供一个全局态的store和,路由组件react-router通过Context管理路由状态等。

当需要在组件树中通过逐层传递props或者state的方式来传递数据时,可以使用Context来实现跨层级的组件数据传递。

1.png

2.png

就类似于Vue中的provider和inject,但是在React中需要自己实现逻辑,将提供者放在一份单独的js文件,使用时导入这份js文件。要Context发挥作用,需要用到两种组件一个是Context生产者(Provider)通常是一个父节点,另外是一个Context的消费者通常是一个或者多个子节点,所以Context的使用基于生产者消费者模式

使用Context

创建一个上下文的容器(组件)

import React from "react";
// 创建上下文对象,提供者
const userContext=React.createContext();
export default userContext;

通过静态方法React.createContext()创建一个Context对象,这个Context对象包含两个组件,<Provider /><Consumer />

import React from "react";
// 引入上下文对象文件
import userCtx from './userCtx.js'
import First from "./First";
class App extends React.Component {

    render() {
       return (
            <div>
                <p>爷爷组件</p>
                <userCtx.Provider value={{ username: '6ara 4ever', info: 'yyds' }}>
                    <First></First>
                </userCtx.Provider>
            </div>

        )
    }
}
export default App;


 import Des from "./des"

import React, { Component } from 'react'

export default class First extends Component {
  render() {
    return (
      <div>
        <p>子组件</p>
        <Des></Des>
      </div>
    )
  }
}

Provider(生产者),用于生产共享数据的地方。Provider 接收一个 value 属性,并将其传递给消费组件。

import React, { Component } from 'react'
import userCtx from './userCtx.js'
export default class Des extends Component {

  render() {
    return (
      <div>
        <userCtx.Consumer>
          {
            (ctxdata) => {
              return (
                <div>
        <p>孙组件</p>
        <userCtx.Consumer>
          {
            (ctxdata) => {
              console.log(ctxdata);
              return (
                <div>
                  <h1>{ctxdata.username}</h1>
                  <p>{ctxdata.info}</p>
                </div>
              )
            }
          }
        </userCtx.Consumer>
      </div>
              )
            }
          }
        </userCtx.Consumer>
      </div>
    )
  }
}

Consumer消费者,该组件中消费者必须使用函数接收当前的 context 值,然后返回一个 React 节点。传递给函数的 value 值就是Provider 提供的 value 值。

image.png

可以直接获取Context的地方,除了实例的context属性(this.context),React组件还有很多个地方可以直接访问父组件提供的Context

类组件:

  • constructor(props, context)
  • componentWillReceiveProps(nextProps, nextContext)
  • shouldComponentUpdate(nextProps, nextState, nextContext)
  • componetWillUpdate(nextProps, nextState, nextContext)
  • 所有能访问this的地方都可以使用this.context来获取上下文对象,就可以得到提供者的数据

ps:如果想要用this.context来获取上下文对象必须在接受值的后代组件名.contextType = 引入的上下文对象(即在后代消费者组件导出时给组件添加一个contextType属性,属性值就是引入的上下文对象),这样才能在消费者组件中用this.context来获取上下文对象,否则是个空对象。

import React, { Component } from 'react'
import userCtx from './userCtx.js'
export default class Des extends Component {

  printctx=()=>{
    console.log(this.context,777777);
  }
  render() {
    return (
      <div>
        <p>孙组件</p>
        <userCtx.Consumer>
          {
            (ctxdata) => {
              console.log(ctxdata,20090729);
              return (
                <div>
                  <h1>{ctxdata.username}</h1>
                  <p>{ctxdata.info}</p>
                  <button onClick={this.printctx}>print context</button>
                </div>
              )
            }
          }
        </userCtx.Consumer>
      </div>
    )
  }
}
 import Des from "./des"

import React, { Component } from 'react'
import userContext from "./userCtx";
 class First extends Component {
  print=()=>{
    console.log(this.context,666666);
  }
  render() {
    return (
      <div>
        <p>子组件</p>
        <Des></Des>
        <button onClick={this.print}>print ctx 子组价</button>
      </div>
    )
  }
}
First.contextType=userContext;
export default First

image.png