优化 React Functional Component 性能

Functional Component 的引入为 React 的开发带来了极大的便利,然而它并不像 Class Component 那样可以设置 shouldComponentUpdate

测试代码如下

import React from "react";

const ItemX = id => {
  return props => {
    console.log(`Item${id} render`);
    const { title } = props;
    let timer = null;

    return <div>{title}</div>;
  };
};

const Item1 = ItemX(1);

class App extends React.Component {
  state = {
    title1: 1,
    x: 3
  };

  _handleClickIncX = () => {
    const { x } = this.state;
    this.setState({
      x: x + 1
    });
  };

  _handleClickIncTitle1 = () => {
    const { title1 } = this.state;
    this.setState({
      title1: title1 + 1
    });
  };

  render() {
    const { title1, title2, title3 } = this.state;
    return (
      <div>
        <Item1 title={title1} />
        <button onClick={this._handleClickIncX}>incX</button>
        <button onClick={this._handleClickIncTitle1}>incTitle1</button>
      </div>
    );
  }
}

可以发现点击 incX 也会导致 Item1 的执行,这就很尴尬了,那么如何让 Functional Component 也具有 shouldComponentUpdate 的功能呢?

我们可以用 PureComponentFunctional Component 包裹起来。

function pure1(component) {
  return class extends React.PureComponent {
    render() {
      const props = this.props;
      return component(props);
    }
  };
}

const Item2 = pure1(ItemX(2));

class App extends React.Component {
  state = {
    title1: 1,
    title2: 2,
    x: 3
  };

  _handleClickIncTitle2 = () => {
    const { title2 } = this.state;
    this.setState({
      title2: title2 + 1
    });
  };

  render() {
    const { title1, title2 } = this.state;
    return (
      <div>
        <Item1 title={title1} />
        <Item2 title={title2} />
        <button onClick={this._handleClickIncX}>incX</button>
        <button onClick={this._handleClickIncTitle1}>incTitle1</button>
        <button onClick={this._handleClickIncTitle2}>incTitle2</button>
      </div>
    );
  }
}

点击 incX 和 incTitle1 并不会导致 Item2 被执行。

项目开发中可以使用 recompose 中的 pure 函数来包裹 Functional Component


然而 pure1 本质上是用一个 Class Component 包裹 Functional Component,这样似乎并不那么 Functional,那么换种方法呢?

const hasOwnProperty = Object.prototype.hasOwnProperty;

function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y;
  } else {
    return x !== x && y !== y;
  }
}

function shallowEqual(objA, objB) {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== "object" ||
    objA === null ||
    typeof objB !== "object" ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}

function pure2(component) {
  let isInitialized = false;
  let oldProps = null;
  let oldValue = null;

  return function(newProps) {
    if (!isInitialized) {
      isInitialized = true;
      oldProps = newProps;
      oldValue = component(newProps);
      return oldValue;
    }

    if (shallowEqual(oldProps, newProps)) {
      return oldValue;
    } else {
      oldProps = newProps;
      oldValue = component(newProps);
      return oldValue;
    }
  };
}

const Item3 = pure2(ItemX(3));

class App extends React.Component {
  state = {
    title1: 1,
    title2: 2,
    title3: 3,
    x: 3
  };

  _handleClickIncTitle3 = () => {
    const { title3 } = this.state;
    this.setState({
      title3: title3 + 1
    });
  };

  render() {
    const { title1, title2, title3 } = this.state;
    return (
      <div>
        <Item1 title={title1} />
        <Item2 title={title2} />
        <Item3 title={title3} />
        <button onClick={this._handleClickIncX}>incX</button>
        <button onClick={this._handleClickIncTitle1}>incTitle1</button>
        <button onClick={this._handleClickIncTitle2}>incTitle2</button>
        <button onClick={this._handleClickIncTitle3}>incTitle3</button>
      </div>
    );
  }
}

shallowEqual 这个函数是从 React 源码中 PureComponent 那部分提取出来的。

Nice Work!