React 源码漂流(三)之 PureComponent

1、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

  • 实现

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

  • 使用场景

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

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

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

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

    React.PureComponent 中的 shouldComponentUpdate() 将跳过全部子组件树的 prop 更新(具体缘由参考 Hooks 与 React 生命周期:即:更新阶段,由父至子去判断是否须要从新渲染),因此使用 React.PureComponent 的组件,它的全部 子组件也必须都为 React.PureComponent性能优化

2、PureComponent 与 Stateless Functional Component

对于 React 开发人员来讲,知道什么时候在代码中使用 ComponentPureComponent Stateless Functional Component 很是重要。数据结构

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

无状态组件

输入输出数据彻底由 props 决定,并且不会产生任何反作用。dom

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 视图,其中从新渲染成本可有可无。它们提供更清晰的代码和更少的文件来处理。

3、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 方法的返回值相反。

4、使用 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 都不会被从新渲染了 👍

误区二:在渲染方法中派生 state

考虑一个文章列表,您的我的资料组件将从中显示用户最喜欢的 10 个做品。

render() {
  const { posts } = this.props
  // 在渲染函数中生成 topTen,并渲染
  const topTen = [...posts].sort((a, b) => 
    b.likes - a.likes).slice(0, 9)
  return //...
}
// 这会致使组件每次从新渲染,都会生成新的 topTen,致使没必要要的渲染

topTen每次组件从新渲染时都会有一个全新的引用,即便 posts 没有更改,派生 state 也是相同的。

这个时候,咱们应该将 topTen 的判断逻辑提取到 render 函数以外,经过缓存派生 state 来解决此问题。

例如,在组件的状态中设置派生 state,并仅在 posts 已更新时更新。

componentWillMount() {
  this.setTopTenPosts(this.props.posts)
}
componentWillReceiveProps(nextProps) {
  if (this.props.posts !== nextProps.posts) {
    this.setTopTenPosts(nextProps.posts)
  }
}
// 每次 posts 更新时,更新派生 state,而不是在渲染函数中从新生成
setTopTenPosts(posts) {
  this.setState({
    topTen: [...posts].sort((a, b) => b.likes - a.likes).slice(0, 9)
  })
}

总结

在使用 PureComponent 时,请注意:

  • 突变通常是很差的,但在使用 PureComponent 时,问题会更加复杂。
  • 不要在渲染方法中建立新函数、对象或数组,这不致使项目性能显著下降。

5、PureComponent 源码解析

// 新建了空方法ComponentDummy ,ComponentDummy 的原型 指向 Component 的原型;
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

/**
 * Convenience component with default shallow equality check for sCU.
 */
function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
} // 解析同 React.Component,详细请看上一章

/**
 * 实现 React.PureComponent 对 React.Component 的原型继承
 */
/**
 * 用 ComponentDummy 的缘由是为了避免直接实例化一个 Component 实例,能够减小一些内存使用
 *
 * 由于,咱们这里只须要继承 React.Component 的 原型,直接 PureComponent.prototype = new Component() 的话
 * 会继承包括 constructor 在内的其余 Component 属性方法,可是 PureComponent 已经有本身的 constructor 了,
 * 再继承的话,形成没必要要的内存消耗
 * 因此会新建ComponentDummy,只继承Component的原型,不包括constructor,以此来节省内存。
 */
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());

// 修复 pureComponentPrototype 构造函数指向
pureComponentPrototype.constructor = PureComponent;

// Avoid an extra prototype jump for these methods.
// 虽然上面两句已经让PureComponent继承了Component
// 但多加一个 Object.assign(),能有效的避免多一次原型链查找
Object.assign(pureComponentPrototype, Component.prototype);

// 惟一的区别,原型上添加了 isPureReactComponent 属性去表示该 Component 是 PureComponent
// 在后续组件渲染的时候,react-dom 会去判断 isPureReactComponent 这个属性,来肯定是否浅比较 props、status 实现更新 
/** 在 ReactFiberClassComponent.js 中,有对 isPureReactComponent 的判断
 if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }
 */
pureComponentPrototype.isPureReactComponent = true;

这里只是 PureComponent 的声明建立,至于如何实现 shouldComponentUpdate() ,核心代码在:

// ReactFiberClassComponent.js
function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
  // ...
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    // 若是是纯组件,比较新老 props、state
    // 返回 true,从新渲染,
    // 即 shallowEqual props 返回 false,或 shallowEqual state 返回 false
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  } 
  return true;
}

shallowEqual.js

/**
 * 经过遍历对象上的键并返回 false 来执行相等性
 * 在参数列表中,当任意键对应的值不严格相等时,返回 false。
 * 当全部键的值严格相等时,返回 true。
 */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  // 经过 Object.is 判断 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;
}

附:Object.is(来自MDN)

Object.is() 判断两个值是否相同

这种相等性判断逻辑和传统的 == 运算不一样,== 运算符会对它两边的操做数作隐式类型转换(若是它们类型不一样),而后才进行相等性比较,(因此才会有相似 "" == false 等于 true 的现象),但 Object.is 不会作这种类型转换。

这与 === 运算符的断定方式也不同。=== 运算符(和== 运算符)将数字值 -0+0 视为相等,并认为 Number.NaN 不等于 NaN

若是下列任何一项成立,则两个值相同:

  • 两个值都是 undefined
  • 两个值都是 null
  • 两个值都是 true 或者都是 false
  • 两个值是由相同个数的字符按照相同的顺序组成的字符串
  • 两个值指向同一个对象
  • 两个值都是数字而且

    • 都是正零 +0
    • 都是负零 -0
    • 都是 NaN
    • 都是除零和 NaN 外的其它同一个数字

走在最后,欢迎关注:前端瓶子君
前端瓶子君

相关文章
相关标签/搜索