React 源码漂流(三)之 PureComponent

一、PureComponent

PureComponent 最早在 React v15.3 版本中发布,主要是为了优化 React 应用而产生。

class Counter 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>
    );
  }
}

在这段代码中, React.PureComponent 会浅比较 props.color 或 state.count 是否改变,来决定是否重新渲染组件。

  • 实现

    React.PureComponentReact.Component 类似,都是定义一个组件类。不同是 React.Component 没有实现 shouldComponentUpdate(),而 React.PureComponent 通过 props 和 state 的 浅比较 实现了。

  • 使用场景

    React.Component 的 props 和 state 均为基本类型,使用 React.PureComponent 会节省应用的性能

  • 可能出现的问题及解决方案

    当props 或 state 为 复杂的数据结构 (例如:嵌套对象和数组)时,因为 React.PureComponent 仅仅是 浅比较 ,可能会渲染出 错误的结果 。这时有 两种解决方案

    • 知道 有深度数据结构更新时,可以直接调用 forceUpdate 强制更新
    • 考虑使用  immutable objects (不可突变的对象),实现快速的比较对象
  • 注意

    React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新(具体原因参考 Hooks 与 React 生命周期:即:更新阶段,由父至子去判断是否需要重新渲染),所以使用 React.PureComponent 的组件,它的所有 子组件也必须都为 React.PureComponent

二、PureComponent 与 Stateless Functional Component

对于 React 开发人员来说,知道何时在代码中使用 ComponentPureComponent Stateless Functional Component 非常重要。

首先,让我们看一下无状态组件。

无状态组件

输入输出数据完全由 props 决定,而且不会产生任何副作用。

const Button = props =>
  <button onClick={props.onClick}>
    {props.text}
  </button>

无状态组件可以通过减少继承 Component 而来的生命周期函数而达到性能优化的效果。从本质上来说,无状态组件就是一个单纯的 render 函数,所以无状态组件的缺点也是显而易见的。因为它没有 shouldComponentUpdate 生命周期函数,所以每次 state 更新,它都会重新绘制 render 函数。

React 16.8 之后,React 引入 Hooks 。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

何时使用 PureComponent

PureComponent 提高了性能,因为它减少了应用程序中的渲染操作次数,这对于复杂的 UI 来说是一个巨大的胜利,因此建议尽可能使用。此外,还有一些情况需要使用 Component 的生命周期方法,在这种情况下,我们不能使用无状态组件。

何时使用无状态组件?

无状态组件易于实施且快速实施。它们适用于非常小的 UI 视图,其中重新渲染成本无关紧要。它们提供更清晰的代码和更少的文件来处理。

三、PureComponent 与 React.memo

React.memo 为高阶组件。它实现的效果与 React.PureComponent 相似,不同的是:

  • React.memo 用于函数组件
  • React.PureComponent 适用于 class 组件
  • React.PureComponent 只是浅比较 propsstateReact.memo 也是浅比较,但它可以自定义比较函数

React.memo

function MyComponent(props) {
  /* 使用 props 渲染 */
}

// 比较函数
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  返回 true,复用最近一次渲染
  返回 false,重新渲染
  */
}

export default React.memo(MyComponent, areEqual);
  • React.memo 通过记忆组件渲染结果的方式实现 ,提高组件的性能
  • 只会对 props 浅比较,如果相同,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
  • 可以将自定义的比较函数作为第二个参数,实现自定义比较
  • 此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,这会产生 bug。
  • 与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

四、使用 PureComponent 常见误区

误区一:在渲染方法中创建函数

如果你在 render 方法里创建函数,那么使用 props 会抵消使用 React.PureComponent 带来的优势。因为每次渲染运行时,都会分配一个新函数,如果你有子组件,即使数据没有改变,它们也会重新渲染,因为浅比较 props 的时候总会得到 false

例如:

// FriendsItem 在父组件引用样式
<FriendsItem
  key={friend.id}
  name={friend.name}
  id={friend.id}
  onDeleteClick={() => this.deleteFriends(friend.id)}
/> 
// 在父组件中绑定
// 父组件在 props 中传递了一个箭头函数。箭头函数在每次 render 时都会重新分配(和使用 bind 的方式相同)

其中,FriendsItemPureComponent

// 其中 FriendsItem 为 PureComponent
class FriendsItem extends React.PureComponent {
  render() {
    const { name, onDeleteClick } = this.props
    console.log(`FriendsItem:${name} 渲染`)
    return (
      <div>
        <span>{name}</span>
        <button onClick={onDeleteClick}>删除</button>
      </div>
    )   
  }
}
// 每次点击删除操作时,未删除的 FriendsItem 都将被重新渲染

点击查看在线实例

这种在 FriendsItem 直接调用 () => this.deleteFriends(friend.id),看起来操作更简单,逻辑更清晰,但它有一个有一个最大的弊端,甚至打破了像 shouldComponentUpdatePureComponent 这样的性能优化。

这是因为:父组件在 render 声明了一个函数onDeleteClick,每次父组件渲染都会重新生成新的函数。因此,每次父组件重新渲染,都会给每个子组件 FriendsItem 传递不同的 props,导致每个子组件都会重新渲染, 即使 FriendsItemPureComponent。 

避免在 render 方法里创建函数并使用它。它会打破了像 shouldComponentUpdate 和 PureComponent 这样的性能优化。

要解决这个问题,只需要将原本在父组件上的绑定放到子组件上即可。FriendsItem 将始终具有相同的 props,并且永远不会导致不必要的重新渲染。

// FriendsItem 在父组件引用样式
<FriendsItem 
  key={friend.id} 
  id={friend.id} 
  name={friend.name} 
  onClick={this.deleteFriends} 
/>

FriendsItem:

class FriendsItem extends React.PureComponent {
  onDeleteClick = () => {
    this.props.onClick(this.props.id)
  } // 在子组件中绑定
  render() {
    const { name } = this.props
    console.log(`FriendsItem:${name} 渲染`)
    return (
      <div>
        <span>{name}</span>
        <button onClick={this.onDeleteClick}>删除</button>
      </div>
    )   
  }
}
// 每次点击删除操作时,FriendsItem 都不会被重新渲染

点击查看在线实例

通过此更改,当单击删除操作时,其他 FriendsItem 都不会被重新渲染了

相关推荐