Vue Mastery之虚拟dom的patch基本思路(1)

115 阅读1分钟
<style>
  .red {
    color: red;
  }
  .green {
    color: green;
  }
</style>

<div id="app"></div>

<script>
  /**
   * 生成统一虚拟dom的格式
   **/
  function h(tag, props, children) {
    return {
      tag,
      props,
      children,
    };
  }

  function mount(vnode, container) {
    const el = (vnode.el = document.createElement(vnode.tag));
    //props
    if (vnode.props) {
      for (const key in vnode.props) {
        const value = vnode.props[key];
        el.setAttribute(key, value);
      }
    }
    // children
    if (vnode.children) {
      if (typeof vnode.children === "string") {
        el.textContent = vnode.children;
      } else {
        vnode.children.forEach((child) => {
          mount(child, el);
        });
      }
    }
    container.appendChild(el);
  }

  const vdom = h("div", { class: "red" }, [h("span", null, "hello")]);

  mount(vdom, document.getElementById("app"));

  function patch(n1, n2) {
    if (n1.tag === n2.tag) {
      const el = (n2.el = n1.el);
      // props
      const oldProps = n1.props || {};
      const newProps = n2.props || {};
      for (const key in newProps) {
        const oldValue = oldProps[key];
        const newValue = newProps[key];
        if (newValue !== oldValue) {
          el.setAttribute(key, newValue);
        }
      }
      for (const key in oldProps) {
        if (!(key in newProps)) {
          el.removeAttribute(key);
        }
      }

      // children
      const oldChildren = n1.children;
      const newChildren = n2.children;
      if (typeof newChildren === "string") {
        if (typeof oldChildren === " string") {
          if (newChildren !== oldChildren) {
            // 新旧children都是字符串 且新旧的文本不一致
            el.textContent = newChildren;
          }
        } else {
          // 新children是字符串 旧children是数组 直接替换el内的内筒 直接覆盖
          el.textContent = newChildren;
        }
      } else {
        if (typeof oldChildren === "string") {
          // 新children是数组 旧children是文本
          el.innerHTML = "";
          newChildren.forEach((child) => {
            mount(child, el);
          });
        } else {
          // 新旧children都是数组
          const commonLength = Math.min(oldChildren.length, newChildren.length);
          // 通过index 对比公共长度的vdom
          for (let i = 0; i < commonLength; i++) {
            patch(oldChildren[i], newChildren[i]);
          }
          if (newChildren.length > oldChildren.length) {
            // 如果新children的长度大于旧children的长度 则截取新children多余部分且添加dom到节点上
            newChildren.slice(oldChildren.length).forEach((child) => {
              mount(child, el);
            });
          } else if (newChildren.length < oldChildren.length) {
            // 如果新children的长度小于旧children的长度 则截取旧children多余部分且移除dom
            oldChildren.slice(newChildren.length).forEach((child) => {
              el.removeChild(child.el);
            });
          }
        }
      }
    } else {
      // repalce
    }
  }

  /**
   * 模拟点击事件触发对比改变dom
   **/
  setTimeout(() => {
    const vdom2 = h("div", { class: "green" }, [h("span", null, "changed!")]);
    patch(vdom, vdom2);
  }, 2000);
</script>