Vue底层学习4——编译器框架搭建
全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/15006455.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)
作为一个Web前端开发人员,使用Vue框架进行项目开发已经有一阵子,掐指一算,是时候认真探索一下Vue的底层了,以前的了解比较偏理论,这一次打算在弄清基本原理的前提下自己手写Vue中的核心部分,也许这样我才敢说自己“深入理解”了Vue。上一篇完成发布订阅模式的编写,实现
Dep
及Watcher
,但到目前为止涉及视图的部分都是预留的状态,原因是我们还缺乏一个解析视图代码的功能,从本篇开始手撸编译器~
编译原理
为什么要进行编译?因为我们实际在书写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-text
、v-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.js
的constructor
中之前模拟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;