React源码分析 - Diff算法

在《React源码分析 - 组件更新与事务》中的流程图的最后:javascript

diff update

蓝色框框的部分分别是Diff算法的核心代码updateChildren以及processUpdates,经过Diff算法获取了组件更新的updates队列以后一次性进行更新。java

Diff算法的代码(先别着急下面会具体解释算法的主要步骤):node

_updateChildren: function (nextNestedChildrenElements, transaction, context) {
      var prevChildren = this._renderedChildren;
      var removedNodes = {};
      var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, removedNodes, transaction, context);
      if (!nextChildren && !prevChildren) {
        return;
      }
      var updates = null;
      var name;
      var lastIndex = 0;
      var nextIndex = 0;
      var lastPlacedNode = null;
      for (name in nextChildren) {
        if (!nextChildren.hasOwnProperty(name)) {
          continue;
        }
        var prevChild = prevChildren && prevChildren[name];
        var nextChild = nextChildren[name];
        if (prevChild === nextChild) {
          updates = enqueue(updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex));
          lastIndex = Math.max(prevChild._mountIndex, lastIndex);
          prevChild._mountIndex = nextIndex;
        } else {
          if (prevChild) {
            lastIndex = Math.max(prevChild._mountIndex, lastIndex);
          }
          updates = enqueue(updates, this._mountChildAtIndex(nextChild, lastPlacedNode, nextIndex, transaction, context));
        }
        nextIndex++;
        lastPlacedNode = ReactReconciler.getNativeNode(nextChild);
      }
      for (name in removedNodes) {
        if (removedNodes.hasOwnProperty(name)) {
          updates = enqueue(updates, this._unmountChild(prevChildren[name], removedNodes[name]));
        }
      }
      if (updates) {
        processQueue(this, updates);
      }
      this._renderedChildren = nextChildren;
    }
复制代码

《深刻React技术栈》这本书对Diff算法的解释比较好。其实只要记住几个原则以及在具体的计算updates队列的时候的算法优化的点就行了。react

传统的diff算法的复杂度是O(n^3),想要具体的了解能够去看"A Survey on Tree Edit Distance and Related Problems"算法

这种复杂度在实际中应用会爆炸的,虽然如今的电脑的CPU很强,但一个页面也不能这样任性~。数组

对此React的作法是给出合理的假设和方法来让整个diff过程合理简化。浏览器

  • DOM节点跨层级的移动操做的场景是不多见的,能够忽略不计。(合理,能够经过组件的设计来尽可能保证DOM结构的稳定,必要时能够经过CSS的方法来进行DOM在展现上的调整,由于建立、删除以及移动DOM的操做是能少则少,浏览器的每一个DOM节点都是一个大对象,有着不少的方法和属性
  • 同一类的两个组件将会生成类似的树形结构,不一样类的两个组件将会生成不一样的树形结构。(合理,自己组件就有提升页面的可复用性的做用,也就是将结构功能相似的页面结构(或者说类似的DOM树形结构)抽象成一类组件,因此合理的组件抽象就应该知足这条假设)
  • 对于同一层级的一组节点能够经过设置惟一的key来进行区分,从而作到diff的进一步优化。(这个不算是一个假设而是一个提升性能的方法)
  • 对于同一类的两个组件,有可能其Virtual Dom是没有任何变化的。所以React容许开发者经过shouldComponentUpdate()来判断组件是否须要进行diff算法分析。(合理,开发者自己对页面的理解来进一步进行diff的优化,固然这有可能会由于开发者错误的使用shouldComponentUpdate()判断错误了是否须要更新,从而获得了错误的结果.....可是这怪sei ???,写bug了还不老实)

基于上面的几条,在具体的Diff过程当中React只进行分层比较,新旧的树之间只比较同一个层次的节点。节点的操做分为3种:插入、移动和删除。dom

节点移动操做判断的过程,引用《深刻React技术栈》中的话:ide

首先对新集合的节点进行循环遍历,for (name in nextChildren),经过惟一 key 能够判断新老集合中是否存在相同的节点,if (prevChild === nextChild),若是存在相同节点,则进行移动操做,但在移动前须要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操做,不然不执行该操做。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),若是新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其余节点的位置,所以不用添加到差别队列中,即不执行移动操做,只有当访问的节点比 lastIndex 小时,才须要进行移动操做。源码分析

须要注意的是”这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),若是新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其余节点的位置,所以不用添加到差别队列中,即不执行移动操做“这句话。意思是若是一个节点在旧集合中的位置已经在你以前进行判断的最后一个节点的背后,那么这个节点已经在被diff过的节点的后面了和以前的diff过的节点在顺序上就已是正确的了,不须要移动了,反之的节点须要被移动。

另外须要知道的是若是没有给key赋值,React会默认使用的是遍历过程当中的 index 值。这里的index值指的是节点遍历的顺序号,效果等同于有些小伙伴用列表数组的index来当作key。这样实际上是很差的,由于节点的key和节点的位置有关系和节点自己不要紧,也就是若是我一个列表有10个节点,按照遍历的顺序key为1到10,而后我在列表的最开始增长了一个节点,这个时候按照列表遍历的顺序来设置key,则原来的10个节点的key都变了,并且新旧节点的key错误的对上了,要知道key在React中时对一个组件的身份识别的标示,错误或者重复的key会形成React错误的结果......so.......key须要是一个和节点自己有联系的惟一标示。

react的做者之一Paul O’Shannessy有提到:

Key is not really about performance, it’s more about identity (which in turn leads to better performance). Randomly assigned and changing values do not form an identity

你可能会问,上面的diff算法的源码部分没看到key啊,恩,其实每一个component的key会变成nextChildren&prevChildren对象中的name对应的value是component,另外在_reconcilerUpdateChildren中的shouldUpdateReactComponent组件的key也有使用到。

对于新增和删除节点的操做简单来讲:

  • 新增节点就是建立新的节点放在顺序遍历到的位置上。
  • 删除节点则是在该层次遍历结束后,对旧集合进行循环遍历,判断是否在新集合中没有,没有的话,则删除节点。

固然上面说的移动、新增和删除节点的操做,不是立刻执行的,而是收集到updates数组中,而后用processUpdates方法一次性进行具体的DOM的的更新。

processUpdates: function (parentNode, updates) {
    for (var k = 0; k < updates.length; k++) {
      var update = updates[k];
      switch (update.type) {
        case ReactMultiChildUpdateTypes.INSERT_MARKUP:
          insertLazyTreeChildAt(parentNode, update.content, getNodeAfter(parentNode, update.afterNode));
          break;
        case ReactMultiChildUpdateTypes.MOVE_EXISTING:
          moveChild(parentNode, update.fromNode, getNodeAfter(parentNode, update.afterNode));
          break;
        case ReactMultiChildUpdateTypes.SET_MARKUP:
          setInnerHTML(parentNode, update.content);
          break;
        case ReactMultiChildUpdateTypes.TEXT_CONTENT:
          setTextContent(parentNode, update.content);
          break;
        case ReactMultiChildUpdateTypes.REMOVE_NODE:
          removeChild(parentNode, update.fromNode);
          break;
      }
    }
  }
复制代码

其中的节点的具体的操做就是到具体的浏览器的DOM的节点的操做了,举个栗子。

function insertLazyTreeChildAt(parentNode, childTree, referenceNode) {
  DOMLazyTree.insertTreeBefore(parentNode, childTree, referenceNode);
}

var insertTreeBefore = createMicrosoftUnsafeLocalFunction(function (parentNode, tree, referenceNode) {
  if (tree.node.nodeType === 11) {
    insertTreeChildren(tree);
    parentNode.insertBefore(tree.node, referenceNode);
  } else {
    parentNode.insertBefore(tree.node, referenceNode);
    insertTreeChildren(tree);
  }
});
复制代码

Node.insertBefore()就是浏览器DOM操做的API了。

想要跟着具体的Diff的过程来理解的话,推荐单步调试或者看《深刻React技术栈》中的栗子,这里我就不画了.....画图很累的.....网上也很是多相似的搜一下就行了。

本文对key的具体的使用的部分有待进一步深刻。【TBD】

参考资料:

相关文章
相关标签/搜索