从零开发一个react系列更新了React调和算法,欢迎找茬

307 阅读3分钟
原文链接: github.com

这个是从零开始开发一个 React 系列的第六篇。

先行知识

学习这个课程之前呢,需要各位了解 React 的 api,以及做了什么事情。

如果完全不了解的话,不建议您继续往下看。

如果你已经具备了相关 React 的知识,那么就让我们开始吧。

本章要实现的效果

本章主要实现 react 的 dom-diff(调和算法)。

前一章的算法比较简单,就是直接将原来的 DOM 移除,然后重新执行一遍 render。 这对于构建中大型应用是不能接受的。 我们这一章就打算使用 dom-diff 算法进行优化。 网上关于 dom-diff 的文章多如牛毛,我们这个系列主要是实战,这些理论知识可以才别的地方了解。

希望大家对 dom-diff 稍有了解再继续阅读。

开始实现

我们将之前的代码做一些修改,之前是这样的:

function reRender(rootReactElement, rootDOMElement) {
  // 移除之前的节点(之后引入调和算法后进行优化)
  while (rootDOMElement.hasChildNodes()) {
    // 遍历调用componentWillUnMount
    rootDOMElement.removeChild(rootDOMElement.lastChild);
  }
  ReactDOM.render(rootReactElement, rootDOMElement);
}

我们要改成性能更好的写法:

function reRender(vdom, ovdom, el) {
  // 找出虚拟dom的差异部分
  const diffInfo = diff(vdom, ovdom);
  // 将差异部分应用到真实节点
  // 更新虚拟dom
  // 更新旧的虚拟dom
  patch(el, diffInfo);
}

因此我们的工作就是要去实现 diff 和 patch 方法。

实现 diff

前面我们讲了虚拟 dom,虚拟 dom 的本质是一个 js 对象,用来表示真实的 dom。 我们可以根据虚拟 dom,生成唯一的一个真实 dom 结构。

一点点理论:

const vdom = {
  type: "div",
  props: {},
  children: []
};

const ovdom = {
  type: "div",
  props: {
    className: "red"
  },
  children: []
};

上面是两个 vdom,一个是变化前的我们极为 ovdom,另外一个是变化后的我们称之为 vdom。

我们的目标就是比较出两者的差异,然后将差异传给 patch,让 patch 最终给 current dom 打补丁,去生成新的真实 dom。

我们需要一种数据结构来完成这样的事情,在讲解数据结构之前,我们来看下如何给真实 dom 打补丁。 其实无外乎替换,调整顺序,修改属性,修改文本。 我们用一个常量记住这些类型方便使用

const REPLACE = 0; // 替换
const ORDER = 1; // children顺序变更
const PROP = 2; // 属性变更
const TEXT = 3; // 文本变更

我们需要去遍历这个树,然后将每一个节点的信息都存起来。我们深度遍历树,然后一次给节点一个索引。

数据结构形如:

// type的含义前面通过常量声明过了
const node0DiffInfo = { type: 2, props: {} };
const node1DiffInfo = { type: 1, moves: [[{}]] };

const diffInfo = [node1DiffInfo, node1DiffInfo];

diff 的具体算法,我这里直接使用了一个库list-diff2,由于这部分知识讲解起来也要 花点功夫,这篇文章就不做深入介绍了,有兴趣自己研究下。

实现 patch

我们已经拿到 diff info 了,剩下工作就简单了,直接将 diff info 应用到树上就好了。 也是一个深度遍历。

核心代码:

function dfs(el, walker, diffInfo) {
  var currentDiffInfos = diffInfo[walker.index];

  var len = el.childNodes ? el.childNodes.length : 0;
  for (let i = 0; i < len; i++) {
    const child = el.childNodes[i];
    walker.index++;
    dfs(child, walker, diffInfo);
  }

  if (currentDiffInfos) {
    apply(el, currentDiffInfos);
  }
}

apply 的代码比较简单,就是根据之前声明的类型调用原生的 web-api。

代码是这样的:

function apply(el, currentDiffInfos) {
  currentDiffInfos.forEach(function(currentDiffInfo) {
    switch (currentDiffInfo.type) {
      case REPLACE:
        const newNode =
          typeof currentDiffInfo.node === "string"
            ? document.createTextNode(currentDiffInfo.node)
            : ReactDOM.render(currentDiffInfo.node, el.cloneNode(false));

        el.parentNode.replaceChild(newNode, el);
        break;
      case REORDER:
        reorderChildren(el, currentDiffInfo.moves);
        break;
      case PROPS:
        setProps(el, currentDiffInfo.props);
        break;
      case TEXT:
        if (el.textContent) {
          el.textContent = currentDiffInfo.content;
        } else {
          el.nodeValue = currentDiffInfo.content;
        }
        break;
      default:
        throw new Error("未知类型 " + currentDiffInfo.type);
    }
  });
}

总结

感谢你的阅读, 下一节我们[引入 context api] , 文章还未更新~