目录

    前言

    目前的主流框架vue、react 都是通过 virtual dom(虚拟dom)来实现的,通过virtual dom技术提高页面的渲染效率。vue中我们通过在 template 模板中编写html代码,react中我们通过在内部的一个 render 函数里编写html代码,这个函数通过 jsx 编译后,实际会输出一个h函数,也就是我们的 virtual dom(虚拟dom),下面简单来实现一个虚拟dom渲染真实dom,以及更新的方法。

    目标

    主要实现以下三个功能:

    • 通过h函数返回vnodes;
    • 通过 mount 函数将 虚拟dom 挂载到真实节点上;
    • 通过 patch 函数通过 newvnodes 与 oldvnodes比较来实现dom的更新;

    第一步:

    在body标签内创建一个id为app的节点,后面会将虚拟节点挂载到这个节点上,而renderer.js用来实现上面三个功能。

    <body>
      <div id="app"></div>
      <script src="./renderer.js"></script>
    </body>

     第二步:

    编写h函数,用来返回tag(标签元素)、props(属性对象)、children(子节点),简单来说虚拟dom就是一个普通javascript对象。

    // renderer.js
     
    const h = (tag, props, children) => {
      return {
        tag,
        props,
        children
      }
    }

    那么通过这个h函数,我们简单来看看下面一段代码会输出什么?

    const vdom = h("div", {class: "header"}, [
      h("h2", null, "hello world"),
      h("h2", {id: 0}, h("span", null, "啦啦啦啦")) // 当props没有值,必须传一个null,不能不传
    ]);
    console.log(vdom);

    由下图可以看出,通过h函数,给我们返回了一个javascript对象,这个便是 virtual dom(虚拟dom),形成树状节点。

     那么我们拿到这个vnodes,如何挂载到真实节点上呢?下面我们来看看第三步

    第三步:

    我们先创建一个 mount 函数,需要传入两个参数,第一个是我们刚刚通过h函数返回的vnodes,第二个参数是我们需要将这些vnode挂载到哪个节点上,接下来看代码:

    const mount = (vnodes, app) => {
      // 通过vnodes里面的tag值,比如("div", "h2"),创建一个节点
      // 同样在vnodes对象里保存一份真实dom,方便以后进行更新,新增等操作
      const el = vnodes.el = document.createelement(vnodes.tag); 
      // 拿到这个节点后,我们通过判断props值,进行添加属性
      if (vnodes.props) {
        for (let key in vnodes.props) {
          // 这儿通过拿到props中key值后,在做判断
          let value = vnodes.props[key];
          if (key.startswith('on')) {
            // 比如用户写了一个onclick="changedata",处理为监听函数的事件
            el.addeventlistener(key.slice(2).tolowercase(), value)
          } else {
            el.setattribute(key, value)
          }
          // 这下面还有些判断,比如指令啊v-if等等,进行边界化处理
        }
      }
      // 处理完props后,最后是children节点
      if (vnodes.children) {
        if (typeof vnodes.children === 'string') {
          // 如果这儿是个字符串类型,那么就可以直接添加到节点中去
          el.textcontent = vnodes.children
        } else {
          // 这种情况为数组类型,含有子节点,通过遍历,再生成子节点
          vnodes.children.foreach(vnode => {
            // 通过递归,再次将子节点挂载到当前节点上去
            mount(vnode, el)
          })
        }
      }
      // 最终将这个真实节点挂载到我们传入的app节点中去
      app.appendchild(el);
    }
    const app = document.queryselector("#app")
    mount(vdom, app)

    我们来看看通过mount函数挂载的实际效果:

     那么到这里,我们就已经实现了通过虚拟dom来创建真实dom了,那在vue当中是如何对这些dom进行更新操作呢,接下来我们再创建一个 patch函数(更新):

    第四步:

    通过patch函数,我们需要传入两个参数(vnodes1、vnodes2),分别是新的虚拟dom和旧的虚拟dom,通过比较新旧虚拟dom,来指定更新哪些节点。(这儿不考虑key值,需要参考key值可以查看链接:)

    // patch函数 n1: 旧节点、 n2:新节点
    // 在vue源码中,旧vnode,新vnode分别用n1, n2表示
    const patch = (n1, n2) => {
      // 在上面我们通过mount函数给n2添加了节点属性el,绑定到n2上
      const el = n2.el = n1.el
      // 首先,还是从两个中的tag入手
      if (n1.tag == n2.tag) {
        // n1、n2的tag相同,再对比props
        const n1props = n1.props || {};
        const n2props = n2.props || {};
        // 分别取到n1,n2中的props,进行比较
        for (let key in n2props) {
          // 取出n2中所有key,判断n2的key值和n1key值是否相同
          const n1value = n1props[key] || '';
          const n2value = n2props[key] || '';
          if (n1value !== n2value) {
            if (key.startswith('on')) {
              // 比如用户写了一个onclick="changedata",处理为监听函数的事件
              el.addeventlistener(key.slice(2).tolowercase(), n2value)
            } else {
              el.setattribute(key, n2value)
            }
          }
          // 相同则不作处理
        }
        for (let key in n1props) {
          const oldvalue = n1props[key];
          if (!(key in n2props)) {
            if (key.startswith('on')) {
              el.removeeventlistener(key.slice(2).tolowercase(), oldvalue)
            } else {
              el.removeattribute(key)
            }
          }
        }
      } else {
        // tag不同,拿到n1的父节点
        const n1parent = n1.el.parentelement;
        // 通过removechild将旧节点从父节点中移除,然后将n2挂载到父节点中
        n1parent.removechild(n1.el); //n1.el是通过mount函数往对象里添加的真实dom节点
        mount(n2, n1parent)
      }
      // 最后处理children,相对于来说复杂些
      // children可以为字符串也可以为数组,那么先看字符串时怎么处理
      const n1children = n1.children || [];
      const n2children = n2.children || [];
      if (typeof n2children === "string") {
        // 如果新节点内容为字符串,直接使用innerhtml进行替换
        el.innerhtml = n2children;
      } else {
        // 下面情况是n2.children为数组情况时
        if (typeof n1.children === "string") {
          // n1.children为字符串,n2.children为数组
          el.innerhtml = ''; // 先将节点内容情况,再讲新的内容添加进去
          mount(n2.children, el)
        } else {
          // 两种都为数组类型时,这儿不考虑key值
          const minlength = math.min(n1children.length, n2children.length);
          for (let i = 0 ; i < minlength ; i++) {
            patch(n1children[i], n2children[i]);
          }
          if(n2children.length > n1children.length) {
            n2children.slice(minlength).foreach(item => {
              mount(item, el)
            })
          }
          if(n2children.length < n1children.length) {
            n1children.slice(minlength).foreach(item => {
              el.removechild(item.el)
            })
          }
        }
      }
    }

    上面简单的实现了patch的作用,其实就是我们说的diff算法(当然这儿没有考虑key值的情况,只能两个依次比较),同一层级进行比较。现在模拟演示一下,看看是否能更新成功:

    const vdom = h("div", {class: "header"}, [
      h("h2", null, "hello world"),
      h("h2", {id: 0}, [h("span", null, "啦啦啦啦")]) // 当props没有值,必须传一个null,不能不传
    ]);
    const app = document.queryselector("#app")
    mount(vdom, app)
    settimeout(()=> { // 3秒后向patch传入新旧vnodes
      const vdom1 = h("div", {class: "header"}, [
        h("h3", null, "hello world"),
        h("span", null, "哈哈哈")
      ])
      patch(vdom, vdom1)
    },3000)

     通过下图,我们可以看到已经简单的实现了虚拟dom更新节点。

    总结

    简单的实现了下虚拟dom生成真实节点,然后通过patch进行更新。再去看看源码,就能更好的理解vue的渲染器是如何实现的了。

    到此这篇关于mini-vue渲染的简易实现的文章就介绍到这了,更多相关mini-vue渲染内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!