上节课我们学习了关于context的使用,但是实际上呢我们在开发中几乎不用哪个东西,我们基本只在封装react插件的时候才会用。这节课呢我们来讲关于组件优化相关的东西

回顾

在开始这节课的内容之前,我们先对上节课的内容来做一个简单的回顾:

  • context可以让祖组件后代组件之间通信
  • 通过React.createContext方法创建Context组件
  • 通过<Xxxxx.Provider value={{key:value}}>包裹组件标签来给组件以及其子组件传递context
  • 通过在组件中声明static contextType来接收context,但是只适用于类组件
  • 通过<Xxxxx.Consumer>标签包裹函数来使用context的数据,还方法使用与所有组件

以上便是上节课的内容,接下来我们来开始这节课的学习

案例

我们先通过一个案例来引出我们这节课的内容

export default class Parent extends Component {
  state = { sum: 0 }
  render() {
    return (
      <div className="parent">
        <h3>Parent Component</h3>
        <span>sum: {this.state.sum}</span><br>
        <button onClick={this.handleClick}>click me</button>
        <Child />
      </div>
    );
  }
  handleClick = () => this.setState(state => ({ sum: state.sum + 1 }));
}

class Child extends Component {
  render() {
    return (
      <div className="child">
        <h3>Child Component</h3>
      </div>
    );
  }
}

我们来看着段代码,还是点击加一的案例,这段代码中我们写了两个组件,并且形成父子关系,然后我们加了一点样式,我们看一下效果:

image-20220121094612777

这就是我们这个效果,可以看出两个组件的层级关系。而且点击也没有问题,但是现在我们要把这个sum传给子组件展示,是不是可以通过props传给子组件?

export default class Parent extends Component {
  state = { sum: 0 }
  render() {
    const {sum} = this.state;
    return (
      <div className="parent">
        <h3>Parent Component</h3>
        <span>sum: {sum}</span><br/>
        <button onClick={this.handleClick}>click me</button>
        <Child sum={sum}/>
      </div>
    );
  }
  handleClick = () => this.setState(state => ({ sum: state.sum + 1 }));
}

class Child extends Component {
  render() {
    return (
      <div className="child">
        <h3>Child Component</h3>
        <span>{this.props.sum}</span>
      </div>
    );
  }
}

我们现在把sum传给了子组件,而且在子组件中也做了展示,那么问题来了,我们点击按钮的话,子组件中的sum会不会变呢?我们来看一下:

image-20220121095011945

我们来看,子组件中的数据是不是也跟着变了?为什么?我们点击按钮是不是会改state?改了state是不是要重新渲染页面?那么重新渲染的时候是不是会把新的state通过props传给子组件?所以说子组件也会变。

但是我们现在有一个问题,我现在不给子组件传props,我们来看代码:

export default class Parent extends Component {
  state = { sum: 0 }
  render() {
    console.log("Parent------");
    const {sum} = this.state;
    return (
      <div className="parent">
        <h3>Parent Component</h3>
        <span>sum: {sum}</span><br/>
        <button onClick={this.handleClick}>click me</button>
        <Child />
      </div>
    );
  }
  handleClick = () => this.setState(state => ({ sum: state.sum + 1 }));
}

class Child extends Component {
  render() {
    console.log("Child------");
    return (
      <div className="child">
        <h3>Child Component</h3>
      </div>
    );
  }
}

我们现在已经不给Child组件传递props了,那么我们点击按钮的时候state改变,父组件的render调用,那么子组件的render会调用吗?我们来看一下:

image-20220121095650264

是不是每次点击按钮两个组件的render都被调用了啊?那么这个情况是不是不太合理?我们子组件根本没有用父组件的东西,凭什么还调用render呢?那么我们再看这种情况:

export default class Parent extends Component {
  state = { sum: 0 }
  render() {...}
  handleClick = () => this.setState({});
}

class Child extends Component {...}

这是什么意思?是不是我们修改state,但是实际上啥都没改?那么我们再看一下结果:

image-20220121100215784

实际上的结果呢就是虽然我们没有修改任何状态,但是我们的这两个组件的render还是都被调用了,这很不合理啊,按照理论上来说我们是不是期望当stateprops真正有改变的时候才重新调用render?但是现在啥都没变还调用render这就很不合理,一部分效率算是被白白浪费掉了。所以我们得出两个结论:

  • Component中只要调用了setState,不管state有没有改变都会重新调用render
  • Component中的render只要被重新调用就会导致子组件的render重新调用

但是这是为什么呢?因为shouldComponentUpdate这个生命周期钩子一直返回的都是true,可是我们不想要上面的这种结果,我们只想要state或者props发生改变了之后再调render那么我们应该怎么处理呢?

export default class Parent extends Component {
  state = { sum: 0 }
  shouldComponentUpdate(nextProps, nextState) {
    console.log(nextProps, nextState);
    console.log(this.props, this.state);
  }
  render() {
    console.log("Parent------");
    const { sum } = this.state;
    return (
      <div className="parent">
        <h3>Parent Component</h3>
        <span>sum: {sum}</span><br />
        <button onClick={this.handleClick}>click me</button>
      </div>
    );
  }
  handleClick = () => this.setState(state => ({sum: state.sum + 1}));
}

我们来看这段代码,我们先不渲染Child组件,我们说了原因是shouldComponentUpdate一直返回的都是true,那么我们就现在这个钩子上做点文章,其实这个钩子是可以接收到两个参数的,一个是下一次的props另一个是下一次的state,那么我们来打印出来这俩参数,以及我们当前的state是什么:

image-20220121101807614

首先,我们组件自身没有props,所以说当前和下一次的props都是空对象,而state我们发现了当前的state和下一次的state不一样,那么这个时候我们是不是应该返回true来让组件更新?那么我们应该这么处理呢?

export default class Parent extends Component {
  state = { sum: 0 }
  shouldComponentUpdate(nextProps, nextState) {
    if (this.state.sum !== nextState.sum) {
      return true
    }
    return false
  }
  render() {
    console.log("Parent------");
    const { sum } = this.state;
    return (
      <div className="parent">
        <h3>Parent Component</h3>
        <span>sum: {sum}</span><br />
        <button onClick={this.handleClick}>click me</button>
      </div>
    );
  }
  handleClick = () => this.setState({});
}

我们来看这段代码,我们是不是判断了当前的state中的sum属性和下一次statesum属性是不是相同,如果不同才返回true,否则返回false,那么这次我们让setState不修改任何数据,那么我们来看一下:

iShot2022-01-21 10.31.19

这一次,无论我们怎么点击,state是不是压根没有变?没有变的话钩子返回false,那么我们的render是不是就不会重新调用了?那么我们再来说一下Child组件:

export default class Parent extends Component {
  ...
  render() {
    console.log("Parent------");
    const { sum } = this.state;
    return (
      <div className="parent">
        ...
        <Child sum={0} />
      </div>
    );
  }
  ...
}
class Child extends Component {
  shouldComponentUpdate(nextProps) {
    if (this.props.sum !== nextProps.sum) {
      return true
    }
    return false
  }
  render() {
    console.log("Child--------");
    return (
      <div className="child">
        <h3>Child Component</h3>
        <span>{this.props.sum}</span>
      </div>
    );
  }
}

我们来看这段代码,我们是不是也是一样的道理?我们判断propsnextProps,但是我们这次给Child传一个固定值作为props那么我们来看一下结果:

image-20220121103948655

我们现在来看,我们的父组件的state变了,但是子组件的props并没有变,所以说一直在调用Parent组件的render方法,但是Child组件的render方法值在第一次加载的时候渲染了一次。

PureComponent

现在我们是不是已经实现了我们之前所说的要求,只有在state或者props发生改变了再重新调用render,那么大家再来思考一个问题,如果我state或者props里面有很多属性怎么办呢?我们还一个一个属性去判断吗?

当然不是,其实react里面给我提供了一个东西叫PureComponent,我们以后再要定义类组件就不再继承自Component了,那么我们继承什么呢?我们继承PureComponent这个东西不需要我们自己写shouldComponentUpdate钩子,他会自己去判断,我们来试一下:

import React, { PureComponent } from 'react';
import './index.css';

export default class Parent extends PureComponent {
  state = { sum: 0 }
  render() {
    console.log("Parent------");
    const { sum } = this.state;
    return (
      <div className="parent">
        <h3>Parent Component</h3>
        <span>sum: {sum}</span><br />
        <button onClick={this.handleClick}>click me</button>
        <Child sum={0} />
      </div>
    );
  }
  handleClick = () => this.setState(state => ({ sum: state.sum + 1 }));
}

class Child extends PureComponent {
  render() {
    console.log("Child--------");
    return (
      <div className="child">
        <h3>Child Component</h3>
        <span>{this.props.sum}</span>
      </div>
    );
  }
}

我们现在把我们之前写的生命周期钩子都删掉了,那么我们还是给Child传一个固定的值:那么我们看一下:

image-20220121105139917

我们来看,是不是依然只调用了Parent组件的render,那么我们在来试一下让setState啥也不修改:

iShot2022-01-21 10.54.48

这一次则不再调用任何render。但是PureComponent做的也是浅对比,和之前redux里面一样。

总结

  • PureComponent自动改写了shouldComponentUpdate钩子
  • PureComponent底层用的是浅对比

Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.

Link of this article:https://work.lynchow.com/article/extend_pure_component/