React shouldComponentUpdate的工作流程

在React中,有一个生命周期为shouldComponentUpdate,用来判定组件是否需要被调停(diff)。该方法会在重新渲染前被触发。其默认实现总是返回 true,让 React 执行更新:

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

shouldComponentUpdate的工作流程

在下图的componets tree中,根节点为C1,子节点为C2、C3,然后它们下面又有着其他的孙节点。

SCU 代表 shouldComponentUpdate 返回的值,而 vDOMEq 代表返回的 React 元素是否相同。

组件树

  • C2节点的 shouldComponentUpdate 返回false,React不会重新渲染C2,以及C2的子节点C4、C5也不会触发再次渲染
  • C1、C3节点 shouldComponentUpdate 返回了true,那么React会继续下询子节点判定如何调停。过程是从C1=>C3,到达C3节点后,发现其子节点C6 SCU 为true,并且vDOMEq为false,判定C6需要调停,则立即进行了更新,更新原则遵从 diff 算法规则。
  • C8节点仍然需要关注, SCU 表示需要调停,但是 vDOMEq 为true(表示和之前渲染的React元素相同),则 vDOMEq 会否定 SCU,则 C8节点会被跳过调停。

手动干预shouldComponentUpdate的返回值

如果我们已知什么情况下某个组件不需要更新,可以通过覆盖shouldComponentUpdate的返回值为false跳过组件重新渲染的过程,提高组件的性能。

shouldComponentUpdate(nextProps, nextState) {
  return nextProps.attr !== this.state.attr;
}

React 帮忙做比较

在上个例子中,shouldComponentUpdate 仅检查了 nextProps.attr vs this.state.attr 是否改变。如果这些值没有改变,那么这个组件不会更新。如果你的组件更复杂一些,你可以使用类似“浅比较”的模式来检查 props 和 state 中所有的字段,以此来决定是否组件需要更新。

React 提供了来帮你实现浅比较的组件 - React.PureComponent

使用方式如下:

// 使用 React.PureComponent 方法创建一个自带 浅比较 的组件
class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

浅比较的过程中,可能发生一些 意想之外 的事情。比如pureComponent中的props是一个复杂数据类型,比如对象类型。

复杂数据类型在创建后会沿用创建时的内存地址,后续对其更改的时候,内存地址是不会发生变化的。所以可能会让React错误的认为该组件的props并未发生变化,导致React不会更新一些 有必要更新 的组件。

比如:


// ListOfWords 是一个自带 浅比较 的组件
class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['Awesome FED']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 问题就出在这里:这部分代码很糟,而且还有 bug
    const words = this.state.words;
    words.push('Awesome FED');

   // 实际上,words是在原内存地址上发生的变化,所以它并不是一个新值,而是一个变化了的老值
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

这样更改复杂数据类型的方式就会导致,上游传递的props无法被下游的pureComponent判定出更改,导致渲染不出最新数据。

所以在更改一些复杂数据类型时,尝试使用 不可变数据(immutable Data)

handleClick() {

  // 数组使用concat的方式,产生的的数据是一个新的引用
  this.setState(state => ({
    words: state.words.concat(['marklar'])
  }));

  // 数组使用ES6 扩展运算符 (对象使用类似)
  this.setState(state => ({
    words: [...state.words, 'marklar'],
    newObj: {...state.newObj, num: 999},
  }));

  // 对象类型可以使用Object.assign
  this.setState(state => ({
    newObj: Object.assign({}, newObj, {num: 999}),
  }));
}

通过以上方式修改复杂数据类型后,新值将不再等同于旧值的内存地址,而是重新开辟了一块空间用于存储,pureComponent 或 shouldComponentUpdate 就可以进行有效的比较了。

以上就是React shouldComponentUpdate的相关工作流程。


1 + 1 =

求知若飢,虛心若愚。