zoukankan      html  css  js  c++  java
  • Vue底层学习4——编译器框架搭建

    全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/15006455.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)

    作为一个Web前端开发人员,使用Vue框架进行项目开发已经有一阵子,掐指一算,是时候认真探索一下Vue的底层了,以前的了解比较偏理论,这一次打算在弄清基本原理的前提下自己手写Vue中的核心部分,也许这样我才敢说自己“深入理解”了Vue。上一篇完成发布订阅模式的编写,实现DepWatcher,但到目前为止涉及视图的部分都是预留的状态,原因是我们还缺乏一个解析视图代码的功能,从本篇开始手撸编译器~

    编译原理

    为什么要进行编译?因为我们实际在书写Vue模板的时候加入了很多浏览器不认识的代码,所以需要进行额外的转换与处理。compile的核心逻辑是获取DOM、遍历DOM,遍历时找到{{}}格式的变量、每个DOM的属性,与此同时截获v-@开头的响应式指令。

    为了方便我们手撸编译器,简化流程后如下图所示,后续编码建议结合下图看思路会更清晰哦~:

    目标功能

    老规矩,先上一个日常开发的例子,帮助我们搞清楚最终需要实现的目标,这里我重新创建了一个demo2的html文件:

    <!-- demo2.html -->
    <!DOCTYPE html>
    <html lang="zh-cn">
    <head>
      <meta charset="UTF-8">
      <title>demo2</title>
    </head>
    <body>
      <div id="app">
        <p>{{name}}</p>
        <p v-text="name"></p>
        <p>{{location}}</p>
        <p>
          {{locationAgain}}
        </p>
        <input type="text" v-model="name" />
        <button @click="changeName">改名儿</button>
        <div v-html="html"></div>
      </div>
    
      <script src="compile.js"></script>
      <script src="MVue.js"></script>
    
      <script>
        const app = new MVue({
          el: '#app',
          data: {
            name: 'dreamsyang',
            location: 'chongqing',
            html: '<button>这是一个按钮</button>'
          },
          created() {
            console.log('开始啦');
            setTimeout(() => {
              this.name = '我是测试';
            }, 1500);
          },
          methods: {
            changeName() {
              this.name = 'hello, dreamsyang!';
              this.location = 'oh, chongqing!';
            }
          }
        })
      </script>
    </body>
    </html>
    

    根据上面的例子汇总3个目标:

    • 目标一:插值绑定,也就是{{}}中的变量绑定,例如{{name}}{{location}}{{locationAgain}}
    • 目标二:指令解析,也就是v-开头的Dom属性,例如v-textv-model(涉及双向绑定的实现)、v-html(涉及html内容解析);
    • 目标三:事件的处理,也就是@开头的Dom属性,例如@click

    编译器框架搭建

    获取Dom

    首先创建一个文件compile.js,也就是目标例子中引入的编译器,主要接收两个参数:el:需要解析的Dom元素选择器,vm:当前的Vue实例。

    /*** compile.js ***/
    // new Compile(el, vm)
    
    class Compile{
      constructor(el, vm) {
        // 需要遍历的Dom节点
        this.$el = document.querySelector(el);
        // 数据缓存
        this.$vm = vm;
      }
    }
    

    遍历子节点

    • 如果获取的Dom节点存在就进行子节点内容提取
      通过document.createDocumentFragment将元素附加到文档片段,因为文档片段存在于内存中,并不在Dom树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算),方便后续编译,减少Dom操作,提高性能。
    /*** compile.js ***/
    // new Compile(el, vm)
    
    class Compile{
      constructor(el, vm) {
        // 需要遍历的Dom节点
        this.$el = document.querySelector(el);
        // 数据缓存
        this.$vm = vm;
    
        // 编译
        if (this.$el) {
          // 提取指定节点中的内容,提高效率,减少Dom操作
          this.$fragment = this.node2Fragment(this.$el);
      }
    
      // 提取指定Dom节点中的代码片段
      node2Fragment(el) {
        const fragment = document.createDocumentFragment();
        // 将el中的所有子元素移动至fragment中
        let child = null;
        while(child = el.firstChild) {
          fragment.appendChild(child);
        }
        return fragment;
      }
    }
    
    • 遍历并判断子节点类型为节点还是插值文本
      编译前先遍历子节点并配合节点的nodeType属性判断节点类型,然后针对不同类型进行对应的编译处理。
    /*** compile.js ***/
    // new Compile(el, vm)
    
    class Compile{
      constructor(el, vm) {
        // 需要遍历的Dom节点
        this.$el = document.querySelector(el);
        // 数据缓存
        this.$vm = vm;
    
        // 编译
        if (this.$el) {
          // 提取指定节点中的内容,提高效率,减少Dom操作
          this.$fragment = this.node2Fragment(this.$el);
          // 执行编译
          this.compile(this.$fragment);
          // 将编译完的html追加至$el
          this.$el.appendChild(this.$fragment);
        }
      }
    
      // 提取指定Dom节点中的代码片段
      node2Fragment(el) {...}
    
      // 编译过程
      compile(el) {
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node => {
          // 类型判断
          if (this.isElement(node)) {
            // 节点
            console.log('编译节点' + node.nodeName);
          } else if(this.isInterpolation(node)) {
            // 插值文本
            console.log('编译插值文本' + node.textContent);
          }
          
          // 递归子节点
          if (node.childNodes && node.childNodes.length > 0) {
            this.compile(node);
          }
        })
      }
    
      isElement(node) {
        return node.nodeType === 1;
      }
    
      isInterpolation(node) {
        return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent);
      }
    }
    
    • demo2中测试一下
      先去掉MVue.jsconstructor中之前模拟Watcher的部分,因为后续属性的getter激活会加入到编译器中,接着初始化一个Compile实例,并将需要解析的Dom元素选择器以及当前的Vue实例作为参数传递进去。
    /*** MVue.js ***/
    // new MVue({ data: {...} })
    
    class MVue {
      constructor(options) {
        // 数据缓存
        this.$options = options;
        this.$data = options.data;
    
        // 数据遍历
        this.observe(this.$data);
    
        new Compile(options.el, this);
      }
    }
    

    运行结果如下,可以看到,我们想要根据不同的节点类型做区别编译的分流已经实现,后续就是实打实的编译操作,且听下回分解:

    参考资料

    1、Document.createDocumentFragment()https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createDocumentFragment
    2、Vue源码:https://github.com/vuejs/vue

  • 相关阅读:
    notepad++中快速插入当前时间方法
    ICE学习笔记一----运行官方的java版demo程序
    使用filter统一设置编码
    hibernate学习笔记之四 Hibernate的增删改查
    hibernate学习笔记之三 持久化的三种状态
    hibernate学习笔记之二 基本环境搭建
    How To Install Proxmox Nested on VMware ESXi (Full Support OpenVZ & KVM)
    struts1四:常用标签
    struts1三:struts1的实现原理
    struts1二:基本环境搭建
  • 原文地址:https://www.cnblogs.com/dreamsqin/p/15006455.html
Copyright © 2011-2022 走看看