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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章