前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React源码学习入门(十一)React组件更新流程详解

React源码学习入门(十一)React组件更新流程详解

作者头像
孟健
发布2022-12-19 17:09:52
6800
发布2022-12-19 17:09:52
举报
文章被收录于专栏:前端工程

React组件更新流程详解

❝本文基于React v15.6.2版本介绍,原因请参见新手如何学习React源码

源码分析

上一篇文章提到最后更新组件是走到了performUpdateIfNecessary方法,让我们来看一看它的实现:

代码语言:javascript
复制
  performUpdateIfNecessary: function(transaction) {
    if (this._pendingElement != null) {
      ReactReconciler.receiveComponent(
        this,
        this._pendingElement,
        transaction,
        this._context,
      );
    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(
        transaction,
        this._currentElement,
        this._currentElement,
        this._context,
        this._context,
      );
    } else {
      this._updateBatchNumber = null;
    }
  },

这个方法其实最终走到的是updateComponent方法,并且注意的是,在我们更新state的当前这个组件,它传入的prev和next都是相同的,这个后面会决定willReceiveProps会不会触发。

接下来就是React组件核心更新方法updateComponent,源码位于src/renderers/shared/stack/reconciler/ReactCompositeComponent.js

代码语言:javascript
复制
  updateComponent: function(
    transaction,
    prevParentElement,
    nextParentElement,
    prevUnmaskedContext,
    nextUnmaskedContext,
  ) {
    var inst = this._instance;

    var willReceive = false;
    var nextContext;

    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context;
    } else {
      nextContext = this._processContext(nextUnmaskedContext);
      willReceive = true;
    }

    var prevProps = prevParentElement.props;
    var nextProps = nextParentElement.props;

    if (prevParentElement !== nextParentElement) {
      willReceive = true;
    }

    // 1. 如果是receive的情况,触发componentWillReceiveProps
    if (willReceive && inst.componentWillReceiveProps) {
      inst.componentWillReceiveProps(nextProps, nextContext);
    }

    // 2. 合并当前的未处理的state
    var nextState = this._processPendingState(nextProps, nextContext);
    var shouldUpdate = true;

    // 3. 计算shouldUpdate
    if (!this._pendingForceUpdate) {
      if (inst.shouldComponentUpdate) {
          shouldUpdate = inst.shouldComponentUpdate(
            nextProps,
            nextState,
            nextContext,
          );
      } else {
        if (this._compositeType === CompositeTypes.PureClass) {
          shouldUpdate =
            !shallowEqual(prevProps, nextProps) ||
            !shallowEqual(inst.state, nextState);
        }
      }
    }

    this._updateBatchNumber = null;
    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      this._performComponentUpdate(
        nextParentElement,
        nextProps,
        nextState,
        nextContext,
        transaction,
        nextUnmaskedContext,
      );
    } else {
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
  },

这个函数核心做了3件事情:

  1. 触发componentWillReceiveProps钩子,这个钩子的触发条件是当context或element发生变化时,显然,刚刚我们进来时发现这里的prev和next都是一样的,也就是触发setState的那个组件是不会调用componentWillReceiveProps的。
  2. 合并当前的未处理的state,这个就是将之前setState插入队列里的state一次性合并到当前的state上,这里的合并用的是Object.assign
  3. 计算shouldUpdate,shouldUpdate默认为true,这也是React最大程度保证了组件都能被更新到,我们可以在组件里面实现自己的shouldComponentUpdate方法来决定是否重新render,另外对于PureComponent来说,这里通过shallowEqual来判断state和props是否发生了变化,主要利用的是Object.is判断是否相等。

当组件被判定为shouldUpdate的时候,就会走到_performComponentUpdate来执行更新:

代码语言:javascript
复制
  _performComponentUpdate: function(
    nextElement,
    nextProps,
    nextState,
    nextContext,
    transaction,
    unmaskedContext,
  ) {
    var inst = this._instance;

    var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
    var prevProps;
    var prevState;
    var prevContext;
    if (hasComponentDidUpdate) {
      prevProps = inst.props;
      prevState = inst.state;
      prevContext = inst.context;
    }

    // 1. 触发willUpdate钩子
    if (inst.componentWillUpdate) {
        inst.componentWillUpdate(nextProps, nextState, nextContext);
    }

    // 2. 更新当前的props和state
    this._currentElement = nextElement;
    this._context = unmaskedContext;
    inst.props = nextProps;
    inst.state = nextState;
    inst.context = nextContext;

    // 3. 更新子组件
    this._updateRenderedComponent(transaction, unmaskedContext);

    // 4. componentDidUpdate入队
    if (hasComponentDidUpdate) {
        transaction
          .getReactMountReady()
          .enqueue(
            inst.componentDidUpdate.bind(
              inst,
              prevProps,
              prevState,
              prevContext,
            ),
            inst,
          );
      }
  },

这个函数主要做了以下几件事:

  1. 触发componentWillUpdate钩子
  2. 更新当前组件实例的props和state
  3. 更新子组件
  4. componentDidUpdate入队,这个和componentDidMount是一样的,都是通过Reconciler的transaction在close阶段按照队列触发。

接下来着重看一下更新子组件的流程:

代码语言:javascript
复制
  _updateRenderedComponent: function(transaction, context) {
    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;
    var nextRenderedElement = this._renderValidatedComponent();

    // shouldUpdateReactComponent,则调用receiveComponent更新子组件
    if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
      ReactReconciler.receiveComponent(
        prevComponentInstance,
        nextRenderedElement,
        transaction,
        this._processChildContext(context),
      );
    } else {
      // 否则,卸载当前的组件重新执行mount流程
      var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
      ReactReconciler.unmountComponent(prevComponentInstance, false);

      var nodeType = ReactNodeTypes.getType(nextRenderedElement);
      this._renderedNodeType = nodeType;
      var child = this._instantiateReactComponent(
        nextRenderedElement,
        nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
      );
      this._renderedComponent = child;

      var nextMarkup = ReactReconciler.mountComponent(
        child,
        transaction,
        this._hostParent,
        this._hostContainerInfo,
        this._processChildContext(context),
      );

      this._replaceNodeWithMarkup(
        oldHostNode,
        nextMarkup,
        prevComponentInstance,
      );
    }
  },

这个函数核心是判断shouldUpdateReactComponent,如果是的话,那就走子组件的更新流程,否则,就销毁子组件,重新挂载。

一般来说,针对子组件的销毁和重建是比较消耗性能的,而且会使得生命周期函数被重复触发,所以React采用一个简单的原则来判断是否需要重新挂载,这也是Diff算法的起点:

代码语言:javascript
复制
function shouldUpdateReactComponent(prevElement, nextElement) {
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }

  var prevType = typeof prevElement;
  var nextType = typeof nextElement;
  if (prevType === 'string' || prevType === 'number') {
    return nextType === 'string' || nextType === 'number';
  } else {
    return (
      nextType === 'object' &&
      prevElement.type === nextElement.type &&
      prevElement.key === nextElement.key
    );
  }
}

解读一下这个关键函数,分几类情况:

  1. emptyComponent的场景,如果同为false或者同为null,则不需要重新挂载,否则重新挂载。
  2. stringnumber的场景,也就是一个文本节点,前后都是文本节点的话,是不需要重新挂载的。
  3. 其他的情况,得看两个组件是否是同一个类型,以及key是否相同,若两个条件同时满足,则不需要重新挂载。

所有触发的子组件,默认按照receiveComponent的模式往下递归,如果遇到React组件,又会重复之前的步骤,它的入口是:

代码语言:javascript
复制
  receiveComponent: function(nextElement, transaction, nextContext) {
    var prevElement = this._currentElement;
    var prevContext = this._context;

    this._pendingElement = null;

    this.updateComponent(
      transaction,
      prevElement,
      nextElement,
      prevContext,
      nextContext,
    );
  },

updateComponent流程上面已经分析过了,不再赘述。

小结一下

本文主要分析了React组件的更新过程,重在几个生命周期函数的触发,以及更新策略,具体真正的更新是在DOMComponent中。我们可以简单总结一下React组件更新的流程图:

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-08-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 孟健的前端认知 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • React组件更新流程详解
    • 源码分析
      • 小结一下
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档