在前一篇文章中我们谈到了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更新

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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。而这一个过程同样也是一个递归的过程。

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 紧接着上面
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)。这个方法的具体实现如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 更新

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 更新

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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,我们可以做以下更新。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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