skip to content
OnionTalk

Deep In React (四) stack reconciliation

在前一篇文章中我们谈到了DOM diff的基石,Internal Instance。同时我们也留下了一些悬而未解解的问题,比如Internal Instance到底有什么更进一步的应用。在这篇文章中,我们来了解一下基于Internal Instance的stack reconciliation(DOM Diff算法)

前提

本篇文章中所有的 Host Component 均以 DOMComponent 为例。

Internal Instance 更新

在之前我们建立了 Internal Instance 的 mount 和 unmount 方法用来处理 Internal Instance 的挂载和卸载。为了完成更新功能,我们需要建立一个叫 receive 的方法。

Composite Component 更新

class CompositeComponent {
  receive(nextElement) {
    const previousElement = this.currentElement;
    const previousProps = previousElement.props;
    const publicInstance = this.publicInstance;
    const previousRenderedComponent = this.renderedComponent;
    const previousRenderedElement = previousRenderedComponent.currentElement;

    // 更新
    this.currentElement = nextElement;
    const type = nextElement.type;
    const nextProps = nextElement.props;

    let nextRenderedElement;

    if (isClass(type)) {
      if (publicInstance.componentWillUpdate) {
        publicInstance.componentWillUpdate(nextProps);
      }

      publicInstance.props = nextProps;

      nextRenderedComponent = publicInstance.render();
    } else if (typeof type === 'function') {
      nextRenderedComponent = type(nextProps);
    }
  }
}

上面的这个 render 方法对应的是当 Composite Component 的 type 没有发生改变的时候,我们只对对应的 component 进行更新,而不是每次有任何更新都进行重新的 mount。而这一个过程同样也是一个递归的过程。

// 紧接着上面的receive 方法
if (previousRenderedElement.type === nextRenderedElement.type) {
  previousRenderedComponent.receive(nextElement);
  return;
}

但是,如果更新时 Composite Component 的 type 发生了变化呢? 比如可能有以下情况,之前渲染的是一个<Button />组件,更新后我们希望渲染一个<List />组件

此时,我们就不能去单纯对 Composite Component 进行更新了。取而代之的是,我们将原有的 Composite Component 进行卸载,然后挂载新的 Composite Component。

// 紧接着上面
if (previousRenderedElement.type === nextElement.type) {
 previousRenderedComponent.receive(nextElement);
 return;
}
// 如果type不一致,那么需要去卸载原有Internal Instance并且挂载新的Internal Instance
 const prevNode = previousRenderedComponent.getHostNode();
 previousRenderedComponent.unMount();
 const nextRenderedComponent = instantiateComponent(nextElement);
 const nextNode = nextRenderedComponent.mount();

 this.renderedComponent = nextRenderedComponent;

 prevNode.parentNode.replaceChild(nextNode, prevNode);
}

总结一下,对于 Composite Component,更新意味着要么去更新原有的 Internal Instance 或者去将原有的 Internal Instance 卸载,挂载新的 Internal Instance。

getHostNode

在上面的代码中,我们调用了一个 getHostNode 方法。这个方法的意图是得到 Internal Instance 的挂载点(不同平台会有差异,比如 ReactDOM 就是 DOM 节点),然后进行一些平台相关的原生操作(比如 replaceChild)。这个方法的具体实现如下。

class CompositeComponent {
  getHostNode() {
    return this.renderedComponent.getHostNode();
  }
}

class DOMComponent {
  getHostNode() {
    return this.node;
  }
}

更新 Host Component

Host Component 会涉及到一些具体平台的原生操作,比如 DOM 操作,同时由于 Host Component 有 children 需要处理。所以更新起来和 Composite Component 略有不同。

Host Component 更新

class DOMComponent {
  receive(nextElement) {
    const node = this.node;
    const prevElement = this.currentElement;
    const prevProps = prevElement.props;
    const nextProps = nextElement.props;

    this.currentElement = nextElement;
    // 更新attribute
    Object.keys(prevProps).forEach((propName) => {
      if (propName !== 'children' && !nextProps.hasOwnProperty(propName)) {
        node.removeAttribute(propName);
      }
    });

    Object.keys(nextProps).forEach((propName) => {
      if (propName !== 'children') {
        node.setAttribute(propName, nextProps[propName]);
      }
    });
  }
}

需要注意的是,在这里,Host Component 并不会发生 type 不一致的情况。原因是 Host Component 根节点的 type 改变会在 Composite Component 的更新时被处理好。

Host Component children 更新

let prevChildren = preProps.children || [];

if (!Array.isArray(prevChildren)) {
  prevChildren = [prevChildren];
}

let nextChildren = nextProps.children || [];

if (!Array.isArray(nextChildren)) {
  nextChildren = [nextChildren];
}

const prevRenderedChildren = this.renderedChildren;
const nextRenderedChildren = [];

// 建立一个操作队列,集中化处理DOM操作
const operationQueue = [];

for (let i = 0; i < nextChildren.length; i++) {
  let prevChild = prevRenderedChildren[i];
  // 如果新的node的位置在之前的DOM树上不存在,意味着是一个单出的新增
  if (!prevChild) {
    const nextChild = instantiateComponent(nextChildren[i]);
    const nextNode = nextChild.mount();

    operationQueue.push({ type: 'ADD', nextNode });
    nextRenderedChildren.push(nextChild);
    continue;
  }

  const canUpdate = prevChildren[i].type === nextChildren[i].type;
  // 如果新老node的类型一样,那么这是一个node的替换
  if (!canUpdate) {
    let prevNode = prevChild.getHostNode();
    prevChild.unmount();

    const nextChild = instantiateComponent(nextChildren[i]);
    const nextNode = nextChildren.mount();

    operationQueue.push({ type: 'REPLACE', prevNode, nextNode });
    nextRenderedChildren.push(nextChild);
    continue;
  }

  // 如果canUpdate, 那么让Internal Instance去处理更新
  prevChild.receive(nextChildren[i]);
  nextRenderedChildren.push(prevChild);
}

// 对于不存在于新的DOM Tree里面的node, 将其删除
for (let j = nextChildren.length; j < prevChildren.length; j++) {
  const prevChild = prevRenderedChild[j];
  const node = prevChild.getHostNode;
  prevChild.unmount();

  operationQueue.push({ type: 'REMOVE', node });
}

// DOM操作队列开始运行
while (operationQueue.length > 0) {
  let operation = operationQueue.shift();
  switch (operation.type) {
    case 'ADD':
      this.node.appendChild(operation.node);
      break;
    case 'REPLACE':
      this.node.replaceChild(operation.prevNode, operation.nextNode);
      break;
    case 'Remove':
      this.node.removeChild(operation.node);
      break;
  }
}

这个队列执行完成,就意味着我们的 Host Component 更新完成了。

回过头来看我们的 mountTree 函数

现在,我们已经实现更新功能了,对于每次 mountTree,我们可以做以下更新。

function mountTree(element, containerNode) {
  if (containerNode.firstChild) {
    var prevNode = containerNode.firstChild;
    var prevRootComponent = prevNode.internalInstance;
    var prevElement = prevRootComponent.currentElemet;
  }
  if (prevElement.type === element.type) {
    prevRootComponent.receive(element);
    return;
  }
  // ...
}

现在每次调用 mountTree 就不会强制摧毁已经存在的 DOM 了。

What’s Next?

在上面我们讨论 Internal Instance 更新时我们忽略了 React 中另一种重要的一种更新机制 — key。同时我们也没有去考虑 state 的变化。省略这些的原因是上面的代码已经比较复杂了,如果引入 key 会让上面的代码变得更加难懂。我们将在下一篇文章中讨论 key 是怎么工作的。

参考资料

React Implementation Detail