1

Deep In React (四) stack reconciliation

 2 years ago
source link: https://hateonion.me/posts/8104/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK