React.Component VS React.PureComponent

源码解读

从判断类组件更新的源码开始。
updateClassComponent()
触发时机:整个项目的任何位置的state, props, context更改,都会导致该函数被触发,且一个类组件就会触发一次,所以其触发的次数就是类组件的数量。
精简后的源码:

/**
* current: 已经用于渲染的fiber
* workInProgress: 正处于更新阶段的fiber
* Component: 当前的组件,组件的代码
* nextProps: 新的props
* renderExpirationTime: 更新的过期时间
*/
  function updateClassComponent(current, workInProgress, Component, nextProps, renderExpirationTime) {
    /**
     * stateNode: {
     *  context: {},
     *  refs: {},
     *  props: {},
     *  state: {},
     *  updater: {
     *    enqueueForceUpdate: fn,
     *    enqueueReplaceState: fn,
     *    enqueueSetState: fn,
     *    isMounted: fn
     *  }
     * }
    */
    var instance = workInProgress.stateNode; //是当前组件的一些信息。
    var shouldUpdate;

    if (instance === null) {
      // Logic for other exceptional cases
    } else {
      // 关注这里,是否要更新由updateClassInstance方法处理
      shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderExpirationTime);
    }
    // 轮转下一个要工作的单元
    var nextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime);
    return nextUnitOfWork;
  }

updateClassInstance()方法:

function updateClassInstance(current, workInProgress, ctor, newProps, renderExpirationTime) {
    var instance = workInProgress.stateNode;
    cloneUpdateQueue(current, workInProgress); // 拷贝一份更新队列
    var oldProps = workInProgress.memoizedProps; // 旧的props
    instance.props = workInProgress.type === workInProgress.elementType ? oldProps : resolveDefaultProps(workInProgress.type, oldProps);
  
    var getDerivedStateFromProps = ctor.getDerivedStateFromProps;
    var hasNewLifecycles = typeof getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function'; // Note: During these life-cycles, instance.props/instance.state are what
    if (!hasNewLifecycles && (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function')) {
      // 触发生命周期ComponentWillReceiveProps
      if (oldProps !== newProps || oldContext !== nextContext) {
        callComponentWillReceiveProps(workInProgress, instance, newProps, nextContext);
      }
    }
    
    resetHasForceUpdateBeforeProcessing();
    var oldState = workInProgress.memoizedState; // 旧的state
    var newState = instance.state = oldState; // 即将存放新的state,这里还是旧值

    // 操作更新队列,同时将新的state更新到workInProgress的memoizedState节点上。
    processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime);
    newState = workInProgress.memoizedState; // 现在是最新的state了

    // 后面解释这里为什么没有返回
    // checkHasForceUpdateAfterProcessing()就是返回代码中是不是有调用强制刷新
    if (oldProps === newProps && oldState === newState && !hasContextChanged() && !checkHasForceUpdateAfterProcessing()) {
        // other
      return false;
    }

    if (typeof getDerivedStateFromProps === 'function') { //触发getDerivedStateFromProps()
      applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps);
      newState = workInProgress.memoizedState;
    }
    
    // 如果代码中有强制更新操作,则不用任何判断都会导致重新render
    // 关注这里
    var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);

    if (shouldUpdate) {
      // 需要更新
      if (!hasNewLifecycles && (typeof instance.UNSAFE_componentWillUpdate === 'function' || typeof instance.componentWillUpdate === 'function')) {
          // 触发声明周期componentWillUpdate
        if (typeof instance.componentWillUpdate === 'function') {
          instance.componentWillUpdate(newProps, newState, nextContext);
        }
  
        if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
          instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
        }
      }
    } else {
       // 无需更新的操作
  
      workInProgress.memoizedProps = newProps;
      workInProgress.memoizedState = newState;
    } // Update the existing instance's state, props, and context pointers even
    // if shouldComponentUpdate returns false.

    // instance就是当前的组件,将新的状态更新到组件的对应节点上
    instance.props = newProps;
    instance.state = newState;
    instance.context = nextContext;
    return shouldUpdate;
  }

这段代码有个地方要单独说明一下

    if (oldProps === newProps && oldState === newState && !hasContextChanged() && !checkHasForceUpdateAfterProcessing()) {
        // other
      return false;
    }

如果满足上面的条件,那么组件也是不会更新的,现在,假设我有2个组件,父组件有两个state,子组件接收其中的一个,当点击父组件按钮时,更新未被子组件接收的那一个state,发现子组件的render也会触发。

    class Parent extends React.Component {
        state = {
            count: 0,
            name: 'home'
        }
        onClick = () => {
            this.setState({
                count: this.state.count+1
            })
        }
        render() {
            return (
                <div>
                    <button onClick={this.onClick} >add count</button>
                    <Child name={this.state.name}/>
                </div>
            )
        }
    }

     class Child extends React.Component {
        render() {
            console.log('我被触发了')
            return (
                <div>
                    <p>{this.props.name}</p>
                </div>
            )
        }
    }

按照源码的逻辑,<Child />组件的stateprops没有更新,也没有context变化,更没有设置强制刷新,那么应该满足条件直接返回false了呀。断点调试发现oldProps === newPropsfalse

截屏2020-10-22 15.56.22.png

继续读源码,发现oldPropsnewProps比较就是workInProgress.memoizedPropsworkInProgress.pendingProps比较,这两个对象的引用地址是不同的,所以这个if条件一般情况下不成立。(都等于null则成立)
接着往下看checkShouldComponentUpdate()方法:

  function checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext) {
    var instance = workInProgress.stateNode;
    // 如果组件显示使用了shouldComponentUpdate,则组件是否需要更新由组件自身决定
    if (typeof instance.shouldComponentUpdate === 'function') {
      var shouldUpdate = instance.shouldComponentUpdate(newProps, newState, nextContext);
      return shouldUpdate;
    }
    // 重点来了,PureComponent和Component的区别就在这里了
    if (ctor.prototype && ctor.prototype.isPureReactComponent) {
      return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState);
    }
    // 不是PureComponent,始终返回true
    return true;
  }

如果是React.PureComponent,则会对该组件的新旧state和新旧props做一个浅比较,注意,只是该组件的props。而如果是React.Component,则只要是父组件的重新render,一定会引起所有子组件的重新render(没有手动控制shouldComponentUpdate),这就是React.PureComponentReact.Component唯一的区别了。

不规范的写法可能导致React.PureComponent 无法正常更新

React.PureComponent什么时候会更新,则完全取决于shallowEqual()会怎么处理了,我遇到过一些场景,本来是希望React.PureComponent可以更新,但是却没有更新,为了保险起见,直接替换为React.Component了,这样做虽然没有问题,但如果理解了React.PureComponent如何更新对于我们理解React行为也是有帮助的,继续看一下shallowEqual()的源码:

  // objectIs就是Object.is, 用法参见https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is
  function shallowEqual(objA, objB) {
    if (objectIs(objA, objB)) {
      return true;
    }

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

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

    if (keysA.length !== keysB.length) {
      return false;
    } // Test for A's keys different from B.


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

    return true;
  }

浅比较对象的过程如下:

  1. Object.is比较引用地址是否发生变化。对于props和state,他们前后的引用地址是不相等的,所以这里一定为false.
  2. 如果是非对象或者null,则返回false,一般不会出现这种情况。
  3. 比较前后两次对象的键的长度,如果不一样,即有新增或者删除属性,则返回false
  4. 遍历对象,比较前后两次对象的键名,如果发生了变化,则返回false。浅比较比较前后两次键值,如果不相等,则返回false

案例1:未浅拷贝对象,导致视图无法更新,比如:

state = {
    person: {name: 'hello'}
}
this.setState({
    person: Object.assign(this.state.person, {
        name: 'jack'
    })
})

按上面的比较过程,1,2,3都不满足,走到第4步,发现前后两次的键名不变,且键值也相等,则判断为相等,最后判断为无需更新。不过对象上的值确实变了,所以如果是继承自React.Component的组件,仍然可以正常看到组件更新。

判断是否需要“更新”后,接下来的工作

到这里我们已经看到了部分生命周期被执行,不过render()方法还未看到,可以沿着updateClassComponent继续往下看,当返回了shouldUpdate标志位之后,控制权交给了finishClassComponent

function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime) {
    // Refs should update even if shouldComponentUpdate returns false
    markRef(current, workInProgress);
    var didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;
    
    if (!shouldUpdate && !didCaptureError) {
      // 如果无需更新,并且当前未发现错误
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderExpirationTime);
    }
    var instance = workInProgress.stateNode; // Rerender
    ReactCurrentOwner$1.current = workInProgress;
    var nextChildren;

    if (didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {
     //....
    } else {
      nextChildren = instance.render(); // 调用组件的render()方法
    }
    workInProgress.memoizedState = instance.state; // The context might have changed so we need to recalculate it.
    return workInProgress.child;
  }

再往后还有一段处理过程,才会到componentDidUpdate()阶段,有兴趣的同学可以自己去看看。

shouldUpdate的作用到这里就结束了,也就是说,React.ComponentReact.PureComponent的区别也就探讨结束了,再总结一下它们的区别:

对于可能引起更新的动作:

  1. state更新
  2. 自身的props更新
  3. 非自身的props更新,但引起了父组件更新。

对于React.Component,在不手动控制shouldComponentUpdate()的情况下,上述三个条件任意一个发生的情况下,有:

  1. componentWillReceiveProps(UNSAFE_componentWillReceiveProps) 或者getDerivedStateFromProps
  2. shouldComponentUpdate
  3. componentWillUpdate(UNSAFE_componentWillUpdate) 或者getSnapshotBeforeUpdate()
  4. render()
  5. componentDidUpdate()

对于React.Component,在不手动控制shouldComponentUpdate()的情况下,1,2两个条件变化和React.Component一样,但条件3表现和React.Component不一样:

  1. componentWillReceiveProps(UNSAFE_componentWillReceiveProps) 或者getDerivedStateFromProps
  2. shouldComponentUpdate

差点被漏掉的context

updateClassInstance中:

    processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime);
    ....
    var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);

checkHasForceUpdateAfterProcessing():

  function checkHasForceUpdateAfterProcessing() {
    return hasForceUpdate;
  }

hasForceUpdate是一个全局变量,而hasForceUpdate的值在调用checkHasForceUpdateAfterProcessing()之前会修改,也就是在processUpdateQueue()里面,调用了getStateFromUpdate()方法:

  function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
    switch (update.tag) {
      case ReplaceState:
        {}

      case CaptureUpdate:
        { }

      case UpdateState: // update.tag: 0
        { }

      case ForceUpdate: // update.tag: 2
        {
          hasForceUpdate = true;
          return prevState;
        }
    }

    return prevState;
  }

当更新来自于context时,会将hasForceUpdate变量置为true,最后导致shouldUpdatetrue

从源码也可以发现,context的变更对于组件是Component还是PureComponent是没有关系的。

最后

意外在getStateFromUpdate()中发现了setState的相关机制,下一篇文章就从这里开始吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352