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 是怎么工作的。