ReactDOM.render做了什么-- 第二部分 更新挂载

1,137 阅读8分钟

先看一下第二部分都是什么

提前说一下: 我们知道React的diff策略中有一条,不要改变组件的最外部元素,比如 原本是p,被改成了div。React的diff策略会认为这两个是完全不一样的组件,即使,两者只是改了一个元素而已。 而这也是 Reactdiff算的复杂度从 O(n^3)将到 O(N)的重要原因。

接下来我们继续来看,根组件的挂啊在这个流程的第二部分,也就是当prevComponent存在的时候,也就是在调用方法

    ReactDOM.render(<App/>,document.getElementById('app')

的时候,id为app的div内已经有内容了,也就是已经被挂载了组件。 这个时候就是要替换的时候了。

我们知道 instancesByReactRootID 内存的是 根实例与根组件id的一一对应的对象。 一般单页面应用,单个 container的话就只有一项。 prevComponent 存在说明,不是初次加载,而是切换根组件

    var prevComponent = instancesByReactRootID[getReactRootID(container)];

    if (prevComponent) {
            
            '
                prevComponent 表示已经存在的根组件,而组件的 _currentElement属性存储的是根组件的 Element表示形式。
                这里的 prevComponent 则是 id为app的根元素包装成的ReactCompositeComponent类的实例。
                一个prevComponent 如下图一所示:
            '
            
            var prevWrappedElement = prevComponent._currentElement;
           
            '
                 这里才是真正的根组件 element信息,比如App组件
            '
        
             var prevElement = prevWrappedElement.props;
          
          '
            shouldUpdateReactComponent方法来判断一下是否要更新
            
          '
          if (shouldUpdateReactComponent(prevElement, nextElement)) {
            '
                如果这两执行了,那就说明,已经存在的根组件,与将要替换他的组件类型一致,只是小改动,这时候就需要用到更新方法。
                _renderedComponent 属性指向一个组件的挂载实例。
                
                挂载组件的 _renderedComponent属性指向的是 一个挂载实例,这个挂载实例是 prevComponent包含的根组件的挂载实例。
                
                publicInst如下图二所示:
            '
            var publicInst = prevComponent._renderedComponent.getPublicInstance();
            
            var updatedCallback = callback && function () {
              callback.call(publicInst);
            };
            '
                调用 更新方法 _updateRootComponent 更新组件。
                _updateRootComponent方法在下边。
            '
            ReactMount._updateRootComponent(prevComponent, nextWrappedElement, container, updatedCallback);
            return publicInst;
          } else {
            '
                到了这里,就将现在存在的根组件直接卸载掉。
            '
            // 直接卸载 container 内的组件
            ReactMount.unmountComponentAtNode(container);
          }
    }

图一

图二:

shouldUpdateReactComponent

prevElement 和 nextElement 一样的话,就返回true。 比如 prevElement和 nextElement 都为null或者是都为false的时候返回true prevElement和 nextElement 都为 string 类型 或者是都为 number 类型的时候返回true prevElement和 nextElement 都为Object类型,切两者的key相等。则返回true、

猜测:返回true的话就说明 要替换 prevElement 的 nextElement 与 prevElement是同一个组件只是内容不一样,可以选择对其进行更新 操作。 而如果 返回了false 说明,要替换的东西nextElement和 prevElement不是一个东西,不如完全的卸载 prevElement, 而后直接将 nextElement挂载上去来的性能高。

    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;
      }
      return false;
    }

getPublicInstance

这个方法就是要返回 挂载实例的 _instance 属性。 而this._instance则是将 当前挂载实例的 Element.type的实例化。 当然是只有自定义的组件才有这个属性。

    getPublicInstance: function () {
        var inst = this._instance;
        if (inst instanceof StatelessComponent) {
          return null;
        }
        return inst;
    },

ReactMount._updateRootComponent 更新渲染的核心方法

这里用到了scrollMonitor方法。 该方法是一个钩子函数,其作用是 在更新的时候 报保持 容器的外观滚动位置不变。

    _updateRootComponent: function (prevComponent, nextElement, container, callback) {
        ReactMount.scrollMonitor(container, function () {
        '
            核心的方法
        '
        ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement);
        if (callback) {
            ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
        }
        });
    return prevComponent;
  },

ReactUpdateQueue.enqueueElementInternal 方法

这个方法如下,核心的作用是 在 组件实例 internalInstance的 属性 _pendingElement 赋值为 要更新的组件。

最后调用了 当前模块下的 enqueueUpdate 方法

    enqueueElementInternal: function (internalInstance, newElement) {
    
        internalInstance._pendingElement = newElement;
        
        enqueueUpdate(internalInstance);
  }

看一下 enqueueUpdate 方法

这个方法很简单 只是调用了 ReactUpdates.enqueueUpdate 这里记一下 internalInstance参数指的是旧的挂载组件,而且旧的挂载组件有个属性上赋值了新的 Element

    function enqueueUpdate(internalInstance) {
        ReactUpdates.enqueueUpdate(internalInstance);
    }

ReactUpdates.enqueueUpdate 方法

这里要说一下,batchingStrategy.isBatchingUpdates 的值为true。

首先。batchingStrategy 是 ReactDefaultBatchingStrategy类 isBatchingUpdates 是类的属性。默认是false。只有在类方法 batchedUpdates 被调用的时候 属性 isBatchingUpdates 的值变为true, 这里的核心是 batchedUpdates 是什么时候调用的。 我们这里上下文中是没有调用 batchedUpdates 方法的。

注意: 在 事件分发机制中我们提到过 dispatchEvent 方法,该方法会调用 ReactUpdates.batchedUpdates,而React组件的更新则是由事件触发的,而不是随意的就进行更新,所以这里的 ReactUpdates.batchedUpdates 方法 调用了 修改了 isBatchingUpdates 为true。

所以该方法就只是 将 internalInstance 放入 dirtyComponents 队列中。

这里 internalInstance 是一个挂在实例 dirtyComponents数组中放置的也就是 挂载实例。

    function enqueueUpdate(component) {
    ensureInjected();
    
    
    if (!batchingStrategy.isBatchingUpdates) {
        batchingStrategy.batchedUpdates(enqueueUpdate, component);
        return;
    }
    
        dirtyComponents.push(component);
    }

到此,就完了,但是,完结的有点奇怪,明明到此为止,根本看不到什么更新的动作。 这里要注意的一点是 事务 事务会在方法执行前后塞入方法来执行。 所以,这里的问题是 究竟用了哪个事务?

这里还是要回说到 事件分发的 dispatchEvent 方法。 这个方法不单单 调用 ReactDefaultBatchingStrategy 类的 batchedUpdates 方法 将 ReactDefaultBatchingStrategy 类的 isBatchingUpdates 属性修改为true,而是同时是调用了一个 Transaction ---- ReactDefaultBatchingStrategyTransaction

ReactDefaultBatchingStrategyTransaction 事务

    transaction.perform(callback, null, a, b, c, d, e);
    

关于事务执行完毕之后要做的事情,我们当然是要看看 Wrapperl了

TRANSACTION_WRAPPERS

主要是以下两个 Wrapper。 第一个只是在 批处理事务执行完毕的时候 将属性 isBatchingUpdates 置为 false。

第二个则是在 事务处理完毕的时候 执行 ReactUpdates.flushBatchedUpdates 方法。 所有,找到,接下来要做的事情在 ReactUpdates.flushBatchedUpdates方法内

    var RESET_BATCHED_UPDATES = {
      initialize: emptyFunction,
      close: function () {
        ReactDefaultBatchingStrategy.isBatchingUpdates = false;
      }
    };
    
    var FLUSH_BATCHED_UPDATES = {
      initialize: emptyFunction,
      close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
    };

我们就可以接着 dirtyComponents 往下走

ReactUpdates.flushBatchedUpdates 方法

    var flushBatchedUpdates = function () {
        '
            首先是遍历 dirtyComponents 队列
        '
      while (dirtyComponents.length || asapEnqueued) {
        if (dirtyComponents.length) {
        '
            调一个 ReactUpdatesFlushTransaction 实例出来
        '
          var transaction = ReactUpdatesFlushTransaction.getPooled();
          '
            使用取出来的事务来执行 runBatchedUpdates 方法
          '
          transaction.perform(runBatchedUpdates, null, transaction);
          ReactUpdatesFlushTransaction.release(transaction);
        }
    
        if (asapEnqueued) {
          asapEnqueued = false;
          var queue = asapCallbackQueue;
          asapCallbackQueue = CallbackQueue.getPooled();
          queue.notifyAll();
          CallbackQueue.release(queue);
        }
      }
    };

先来看看 ReactUpdatesFlushTransaction 事务

核心还是让我们看看 事务的Wrapper

就是在 事务结束的时候,执行callbackQueue 的方法。

   var UPDATE_QUEUEING = {
      initialize: function () {
        this.callbackQueue.reset();
      },
      close: function () {
        this.callbackQueue.notifyAll();
      }
    };
    

ReactUpdatesFlushTransaction 事务的作用很简单,核心还是要看一下事务要包裹的方法 runBatchedUpdates

ReactUpdate的 runBatchedUpdates 方法

    function runBatchedUpdates(transaction) {
        '
            先拿到 dirtyComponents 队列的长度
            这里 transaction 属性 dirtyComponentsLength 是 在事务的 Wrapper 中的 initialize 方法中初始化的。
        '
        var len = transaction.dirtyComponentsLength;
        
        '
            dirtyComponents 进行排序,排序的规则是 dirtyComponents 队列里每一项的 _mountOrder 大小来进行排序的。(从小到大)
        '
        dirtyComponents.sort(mountOrderComparator);
        
        for (var i = 0; i < len; i++) {
            '
                取出来每一项来遍历操作
                component 是一个挂载实例。
                _pendingCallbacks 在下边有详细说明
            '
            var component = dirtyComponents[i];
            var callbacks = component._pendingCallbacks;
            component._pendingCallbacks = null;
            
            '
                调用 performUpdateIfNecessary 方法
                performUpdateIfNecessary是 component 本身所有的方法。
                
            '
            ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);
            
            '
                如果 callbacks 存在就将这些callbacks 入队,待得
            '
            if (callbacks) {
              for (var j = 0; j < callbacks.length; j++) {
                transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
              }
            }
        }
    }
    

_pendingCallbacks

_pendingCallbacks 是挂载实例的一个熟悉,主要用来存储回调函数,一个setState的回调函数

在调用setState的时候会向组件挂载实例的属性 _pendingCallbacks添加一setState方法的第二个参数(回调函数)。 这一点在 setState中有说明

看一下 performUpdateIfNecessary 方法

ReactCompositeComponent 模块下的 performUpdateIfNecessary

这里要说一下,这一系列指示要挂载根组件,所以,dirtyComponents中只会存两种挂载实例: 一 最外层根元素 id为app的div的包装挂载实例

二 根组件 App 的挂载实例。

这两个挂载实例都是 ReactCompositeComponent 的子类。所以可以调用 ReactCompositeComponent 模块下的 performUpdateIfNecessary 方法。

简单的来说 performUpdateIfNecessary 方法的作用是 :

flush 一个组件中的任何 dirty 变化。

React中对这个方法有解释:

If any of _pendingElement, _pendingStateQueue, or _pendingForceUpdate is set, update the component.

也就是三个属性中的任何一个被设定有值就更新组件。 关于这三个属性参考这篇文章

    performUpdateIfNecessary: function (transaction) {
        if (this._pendingElement != null) {
        '
            用一个新 element 来更新组件
        '
          ReactReconciler.receiveComponent(this, this._pendingElement || this._currentElement, transaction, this._context);
        }
        
        if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
        '
            updateComponent方法是对已经安装的组件进行更新,componentWillReceiveProps 和 shouldComponentUpdate 两个方法会被调用,
        '
          this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
        }
    },

_pendingElement

为更新队列设定的属性,是自定义组件的挂载实例的一个属性。初始值为null。 一般情况下其为 null。

这里在 ReactMount._updateRootComponent 方法中调用下边的代码,为 _pendingElement 赋值

    ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement);

ReactReconciler.receiveComponent

这个方法的主要作用是 用一个新的 element来更新一个组件。

    receiveComponent: function (internalInstance, nextElement, transaction, context) {
        
        '
            如果 prevElement (上一个 挂载的 Element) 与 nextElement(接下来要挂载的Element)一样。
            直接返回,因为没啥要改的。
        '
        var prevElement = internalInstance._currentElement;
        
        if (nextElement === prevElement && context === internalInstance._context) {
         
          return;
        }
        
        '
            计算一下是否要更新 refs
        '
        var refsChanged = ReactRef.shouldUpdateRefs(prevElement, nextElement);
        
        '
            移除旧的ref指向,将ref指向新的element的ref属性
        '
        if (refsChanged) {
          ReactRef.detachRefs(internalInstance, prevElement);
        }
        '
            而后调用 组件挂载实例的 receiveComponent方法
        '
        internalInstance.receiveComponent(nextElement, transaction, context);
    
        if (refsChanged && internalInstance._currentElement && internalInstance._currentElement.ref != null) {
          transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
        }
    },

receiveComponent

receiveComponent方法很简单,就是 调用了挂载实例的 更新方法 updateComponent。

    receiveComponent: function (nextElement, transaction, nextContext) {
        var prevElement = this._currentElement;
        var prevContext = this._context;
    
        this._pendingElement = null;
    
        this.updateComponent(transaction, prevElement, nextElement, prevContext, nextContext);
    },

关于 updateComponent 方法,请参考文章 综合组件的更新方法

到此为止,根组件被更新/替换了。 当然我们知道 这些是因为 shouldUpdateReactComponent 方法执行为true的时候。

这个时候是因为要用来替换的根组件与原组件是一个东西,可能只是内容不太一样,所以就采用了更新的方法。这样性能更好

而如果:shouldUpdateReactComponent方法返回false。也就是要用来替换的根组件和原来组件不一样。

直接将当前根组件卸载了,而后重新的将要替换的组件当做新组件进行渲染。

    ReactMount.unmountComponentAtNode(container);

有些啰嗦了。

简言

这一部分针对的是 根组件被替换的时候。 也据说ReactDOM.render方法被重复调用,根元素(id为app的div元素)内的根组件App被替换的时候。所做的事情。

如果是替换一个新的根组件,那就是直接卸载老组件而后将新组件渲染。

而如果是更新,就需要进行更新,一些细致化的操作。