React 更新视图历程

说在前面,偏重梳理现实更新组件和 dom 部份的代码,然则关于异步,transaction,批量兼并新状况等新细节只形貌步骤。一来由于这些细节在读源码的时刻只读了部份,二来假如要把这些都写出来要写老长老长。

实在的 setState 的历程:

setState( partialState ) {
  // 1. 经由过程组件对象猎取到衬着对象
  var internalInstance = ReactInstanceMap.get(publicInstance);
  // 2. 把新的状况放在衬着对象的 _pendingStateQueue 内里 internalInstance._pendingStateQueue.push( partialState )
  // 3. 查看下是不是正在批量更新
  //   3.1. 假如正在批量更新,则把当前这个组件以为是脏组件,把其衬着对象保存到 dirtyComponents 数组中
  //   3.2. 假如能够批量更新,则挪用 ReactDefaultBatchingStrategyTransaction 开启更新事件,举行真正的 vdom diff。
  //    |
  //    v
  // internalInstance.updateComponent( partialState )
}

updateComponent 要领的申明:

updateComponent( partialState ) {
  // 源码中 partialState 是从 this._pendingStateQueue 中猎取的,这里简化了状况行列的东西,假定直接从外部传入
  var inst = this._instance;
  var nextState = Object.assign( {}, inst.state, partialState );
  // 取得组件对象,预备更新,先挪用性命周期函数
      // 挪用 shouldComponentUpdate 看看是不是须要更新组件(这里先疏忽 props 和 context的更新)
  if ( inst.shouldComponentUpdate(inst.props, nextState, nextContext) ) {
    // 更新前挪用 componentWillUpdate
    isnt.componentWillUpdate( inst.props, nextState, nextContext );
    inst.state = nextState;
    // 天生新的 vdom
    var nextRenderedElement = inst.render();
    // 经由过程上一次的衬着对象猎取上一次天生的 vdom
    var prevComponentInstance = this._renderedComponent; // render 中的根节点的衬着对象
    var prevRenderedElement = prevComponentInstance._currentElement; // 上一次的根节点的 vdom
    // 经由过程比较新旧 vdom node 来决定是更新 dom node 照样依据最新的 vdom node 天生一份实在 dom node 替代掉本来的
    if ( shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement) ) {
      // 更新 dom node
      prevComponentInstance.receiveComponent( nextRenderedElement )
    } else {
      // 天生新的 dom node 替代本来的(以下是简化版,只为了申明流程)
      var oldHostNode = ReactReconciler.getHostNode( prevComponentInstance );
      // 依据新的 vdom 天生新的衬着对象
      var child = instantiateReactComponent( nextRenderedElement );
      this._renderedComponent = child;
      // 天生新的 dom node
      var nextMarkup = child.mountComponent();
      // 替代本来的 dom node
      oldHostNode.empty();
      oldHostNode.appendChild( nextMarkup )
    }
  }
}

接下来看下 shouldUpdateReactComponent 要领:

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

基础的思绪就是比较当前 vdom 节点的范例,假如一致则更新,假如不一致则从新天生一份新的节点替代掉本来的。好了回到方才跟新 dom node这条路 prevComponentInstance.receiveComponent( nextRenderedElement ),即 render 内里根元素的衬着对象的 receiveComponent 要领做了末了的更新 dom 的事情。假如根节点的衬着对象是组件即 ReactCompositeComponent.receiveComponent,假如根节点是内置对象(html 元素)节点即 ReactDOMComponent.receiveComponent。ReactCompositeComponent.receiveComponent 终究照样挪用的上面提到的 updateComponent 轮回去天生 render 中的 vdom,这里就先不穷究了。终究 html dom node 的更新战略都在 ReactDOMComponent.receiveComponent 中。

class ReactDOMComponent {
  // @param {nextRenderedElement} 新的 vdom node
  receiveComponent( nextRenderedElement ) {
    var prevElement = this._currentElement;
    this._currentElement = nextRenderedElement;

    var lastProps = prevElement.props;
    var nextProps = this._currentElement.props;
    var lastChildren = lastProps.children;
    var nextChildren = nextProps.children;
    /*
      更新 props
      _updateDOMProperties 要领做了下面两步
      1. 纪录下 lastProps 中有的,nextProps 没有的,删除
      2. 纪录下 nextProps 中有的,且与 lastProps中差别的属性,setAttribute 之
    */
    this._updateDOMProperties(lastProps, nextProps, transaction);

    /*
      迭代更新子节点,源代码中是 this._updateDOMChildren(lastProps, nextProps, transaction, context);
      以下把 _updateDOMChildren 要领睁开,关于子节点范例的推断源码比较复杂,这里只针对string|number和非string|number做一个简朴的流程示例
    */
    // 1. 假如子节点从有到无,则删除子节点
    if ( lastChildren != null && nextChildren == null ) {
    
      if ( typeof lastChildren === 'string' | 'number' /* 伪代码 */ ) {
        this.updateTextContent('');
      } else {
        this.updateChildren( null, transaction, context );
      }
    }
    // 2. 假如新的子节点相关于老的是有变化的
    if ( nextChildren != null ) {
      if ( typeof lastChildren === 'string' | 'number' && lastChildren !== nextChildren /* 伪代码 */ ) {
        this.updateTextContent('' + nextChildren);
      } else if ( lastChildren !== nextChildren ) {
        this.updateChildren( nextChildren, transaction, context );
      }
    }
  }
}

this.updateChildren( nextChildren, transaction, context ) 中是真正的 diff 算法,就不以代码来讲了(由于光靠代码很难申明清晰)

先来看最简朴的状况:
例A:
《React 更新视图历程》
按节点递次最先遍历 nextChildren(遍历的历程当中纪录下须要对节点做哪些变动,等遍历完一致实行终究的 dom 操纵),雷同位置假如遇到和 prevChildren 中 tag 一样的元素以为不须要对节点举行删除,只须要更新节点的 attr,假如遇到 tag 不一样,则根据新的 vdom 中的节点从新天生一个节点,并把 prevChildren 中雷同位置老节点删除。按以上两个状况的 vdom tree,那末遍历完就会纪录下须要做两步 dom 变动:新增一个 span 节点插进去到第二个位置,删除本来第二个位置上的 div。

再来看两个例子:
例B:
《React 更新视图历程》
遍历效果:第二个节点新增一个span,删除第二个div和第四个div。

例C:
《React 更新视图历程》
遍历效果:第二个节点新增一个span,第四个节点新增一个div,删除第二个div。

我们看到关于例C来讲实在最方便的要领就是把 span 插进去到第二的位置上,然后其他div只要做 attr 的更新而不须要再举行位置的增删,假如 attr 都没有变化,那末后两个 div 基础不须要变化。然则按例A内里的算法,我们须要举行好几步的 dom 操纵。这是为算法削减时候复杂度,做了让步。然则 react 对节点引入了 key 这个症结属性协助优化这类状况。假定我们给一切节点都添加了唯一的 key 属性,如下面例D:
例D:
《React 更新视图历程》
我们在遍历历程当中对所要纪录的东西举行优化,在某个位置遇到有 key 的节点我们去 prevChildren 中找有无对应的节点,假如有,则我们会比较当前节点在前后两个 tree 中相对位置。假如相对位置没有变化,则不须要做dom的增删移,而只须要更新。假如位置不一样则须要纪录把这个节点从老的位置挪动到新的位置(细致算法须要借助前一次dom变化的纪录这里不详述)。如许从例C到例D的优化削减了 dom 节点的增删。

然则 react 的这类算法的优化也带来了一种极度的状况:
例E:
《React 更新视图历程》
遍历效果:3次节点位置挪动:2到1,1到2,0到3。

然则实在这里只须要更新每一个节点的 attr,他们的位置基础不须要做变化。所以假如要给元素指定 key 最好防止元素的位置有太多太大的跃迁变化。

基础上 setState 之后到终究的 dom 变化的历程就是这么完毕了。

跋文:
梳理的比较简朴,许多细节我没有精力作逐一的总结,由于我本身看源码看了良久,代码中涉及到许多异步,事件等等滋扰项,然后我本身又不想过量的借助现有的材料-_-。当我快要把末了一点写完的时刻发明 pure render 专栏的作者陈屹出了一本《深切React手艺栈》内里有相称细致的源码剖析,所以我觉得我这篇“白写”了,贴出这本书就能够了,不过陈屹的这本书是良知之作,必需安利下。

    原文作者:Bernie维尼
    原文地址: https://segmentfault.com/a/1190000009430113
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞