一个简易Vue 渲染器的实现

249 阅读3分钟

实现渲染器的基本步骤

    1. 通过h函数生成虚拟DOM(vnode)
    1. 通过mount函数将DOM挂载到页面中去
    1. 通过patch函数对比新旧节点之间的差别,重新生成新的DOM挂在到页面

创建h函数

  • h函数的主要用处为生成虚拟dom,虚拟dom即包含tag(dom标签名),props(标签的属性,绑定的事件等),childrens(主要包含该节点的字元素可能是文本类型也可能是dom节点)
const h = (tag, props, childrens) => {
  return {
    tag,
    props,
    childrens,
  };
};

创建mount函数

  • 首先要明确mount函数中作了哪些事情
      1. 生成真实的dom节点
      1. 为dom节点绑定属性以及事件等
      1. 遍历所有的子节点,完成1,2两步后添加为父节点的子元素
      1. 将生成的dom挂载到指定的dom节点上
// 将虚拟dom映射为真实的dom且挂载到dom中去
const mount = (vnode, container) => {
  const { tag, props, childrens } = vnode;
  // 创建标签
  const el = (vnode.el = document.createElement(tag));
  // 处理props
  if (props) {
    Object.keys(props).forEach((key) => {
      if (key.startsWith("on")) {
        el.addEventListener(key.slice(2).toLowerCase(), props[key]);
      } else {
        el.setAttribute(key, props[key]);
      }
    });
  }
  // 处理childrens
  if (childrens) {
    if (typeof childrens === "string") {
      el.innerText = childrens;
    } else {
      childrens.forEach((child) => {
        mount(child, el);
      });
    }
  }
  // 挂载
  container.appendChild(el);
};

创建patch函数

  • patch函数的主要用处为比较新旧节点之间的差异,然后生成新的节点挂载到dom中去,此处咋就简单处理一下

      1. 比较两个节点的标签是否一致:不一致直接将原节点从dom移除,重新挂载新的节点

      1. 两个节点的标签一致则比较标签的属性以及事件
      1. 对比后代元素
const patch = (v1, v2) => {
  // 首先比较两个dom的标签是否一致
  if (v1.tag !== v2.tag) {
    const v1ParentEl = v1.el.parentElement;
    v1ParentEl.removeChild(v1.el);
    mount(v2, v1ParentEl);
  } else {
    // 取出element对象,保存到v2
    const el = (v2.el = v1.el);

    // 处理新旧节点的props
    const newProps = v2.props || {};
    const oldProps = v1.props || {};

    // 将旧的props中的属性删除,新的props属性添加到元素中
    Object.keys(newProps).forEach((key) => {
      if (key.startsWith("on")) {
        el.addEventListener(key.slice(2).toLowerCase(), newProps[key]);
      } else {
        el.setAttribute(key, newProps[key]);
      }
    });
    Object.keys(oldProps).forEach((key) => {
      if (key.startsWith("on")) {
        el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
      } else {
        el.removeAttribute(key, oldProps[key]);
      }
    });

    // 处理childrens中的元素
    const newChilds = v2.childrens;
    const oldChilds = v1.childrens;
    // 如果newChilds为String
    if (typeof newChilds === "string") {
      // oldChilds也为String
      if (typeof newChilds === "string") {
        if (newChilds !== oldChilds) {
          el.innerText = newChilds;
        }
      } else {
        el.innerText = newChilds;
      }
    } else {
      // 如果newChilds为Array
      if (typeof oldChilds === "string") {
        el.innerHTML = "";
        newChilds.forEach((item) => {
          mount(item, el);
        });
      } else {
        /**
         * newChilds: [v1, v2, v3, v4]
         * oldChilds: [v1, v6]
         * **/
        // 首先将前面相同的节点进行patch
        const commonLength = Math.min(newChilds.length, oldChilds.length);
        for (let i = 0; i < commonLength; i++) {
          patch(oldChilds[i], newChilds[i]);
        }

        // 当newChilds.length > oldChilds.length
        if (newChilds.length > oldChilds.length) {
          newChilds.slice(commonLength).forEach((item) => {
            mount(item, el);
          });
        } else {
          oldChilds.slice(commonLength).forEach((item) => {
            el.removeChild(item.el);
          });
        }
      }
    }
  }
};

写在最后

这样一个简易版的渲染器就实现了,几个函数中都有很多需要处理的边界情况,在这就例举了几种case,只是为了帮助个人学习过程中的消化吸收,更好的理解渲染器的执行过程。

源码

  • html部分
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./render.js"></script>
    <script>
      const vnode1 = h(
        "div",
        {
          id: "wrap",
          class: "wrap-v1",
          onClick: function () {
            alert("Hello mini-vue! vnode1");
          },
        },
        [
          h("button", null, "按钮v-1"),
          h("button", null, "按钮v-2"),
          h("button", null, "按钮v-3"),
        ]
      );
      mount(vnode1, document.querySelector("#app"));
      const vnode2 = h(
        "div",
        {
          id: "wrap",
          class: "wrap-v1",
          onClick: function () {
            alert("Hello mini-vue! vnode2");
          },
        },
        [h("div", null, "vnode2"), h("button", null, "按钮")]
      );

      setTimeout(() => {
        patch(vnode1, vnode2);
      }, 3000);
    </script>
  </body>
</html>
  • js 部分
// 生成虚拟dom
const h = (tag, props, childrens) => {
  return {
    tag,
    props,
    childrens,
  };
};

// 将虚拟dom映射为真实的dom且挂载到dom中去
const mount = (vnode, container) => {
  const { tag, props, childrens } = vnode;
  // 创建标签
  const el = (vnode.el = document.createElement(tag));
  // 处理props
  if (props) {
    Object.keys(props).forEach((key) => {
      if (key.startsWith("on")) {
        el.addEventListener(key.slice(2).toLowerCase(), props[key]);
      } else {
        el.setAttribute(key, props[key]);
      }
    });
  }
  // 处理childrens
  if (childrens) {
    if (typeof childrens === "string") {
      el.innerText = childrens;
    } else {
      childrens.forEach((child) => {
        mount(child, el);
      });
    }
  }
  // 挂载
  container.appendChild(el);
};

// 更新dom
const patch = (v1, v2) => {
  // 首先比较两个dom的标签是否一致
  if (v1.tag !== v2.tag) {
    const v1ParentEl = v1.el.parentElement;
    v1ParentEl.removeChild(v1.el);
    mount(v2, v1ParentEl);
  } else {
    // 取出element对象,保存到v2
    const el = (v2.el = v1.el);

    // 处理新旧节点的props
    const newProps = v2.props || {};
    const oldProps = v1.props || {};

    // 将旧的props中的属性删除,新的props属性添加到元素中
    Object.keys(newProps).forEach((key) => {
      if (key.startsWith("on")) {
        el.addEventListener(key.slice(2).toLowerCase(), newProps[key]);
      } else {
        el.setAttribute(key, newProps[key]);
      }
    });
    Object.keys(oldProps).forEach((key) => {
      if (key.startsWith("on")) {
        el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
      } else {
        el.removeAttribute(key, oldProps[key]);
      }
    });

    // 处理childrens中的元素
    const newChilds = v2.childrens;
    const oldChilds = v1.childrens;
    // 如果newChilds为String
    if (typeof newChilds === "string") {
      // oldChilds也为String
      if (typeof newChilds === "string") {
        if (newChilds !== oldChilds) {
          el.innerText = newChilds;
        }
      } else {
        el.innerText = newChilds;
      }
    } else {
      // 如果newChilds为Array
      if (typeof oldChilds === "string") {
        el.innerHTML = "";
        newChilds.forEach((item) => {
          mount(item, el);
        });
      } else {
        /**
         * newChilds: [v1, v2, v3, v4]
         * oldChilds: [v1, v6]
         * **/
        // 首先将前面相同的节点进行patch
        const commonLength = Math.min(newChilds.length, oldChilds.length);
        for (let i = 0; i < commonLength; i++) {
          patch(oldChilds[i], newChilds[i]);
        }

        // 当newChilds.length > oldChilds.length
        if (newChilds.length > oldChilds.length) {
          newChilds.slice(commonLength).forEach((item) => {
            mount(item, el);
          });
        } else {
          oldChilds.slice(commonLength).forEach((item) => {
            el.removeChild(item.el);
          });
        }
      }
    }
  }
};