介绍
我觉得现在学习前端的人都知道MVVM是什么意思,现在主流的框架React、Vue都使用的MVVM的原理,今天我们就来实现一下MVVM其中的一小部分Compie指令解析。
- compile就是解析的意思,解析我们使用的模板语法。我这篇文章主要是仿照实现Vue的Compile,
效果
- Vue实现的效果
<div id="app">
<input type="text" v-model="a">
<p>{{ b.c }}</p>
<div v-html="c"></div>
</div>
let vm = new Vue({
el : "#app",
data: {
a: 1,
b: {
c: 1
},
c: "<p>165165</p>"
}
});


- 我们模拟实现的效果
<div id="app">
<ul>
<input type="text" v-model="b.c">
<li>{{ a }}</li>
<li>
<span>9</span>
</li>
<div v-html="c"></div>
<div v-html="<b>56156</b>"></div>
{{ b.c }}
</ul>
</div>
let vm = new MVVM({
el: "#app",
data: {
a: 1,
b: {
c: 2
},
c: "<p>56165</p>"
}
});


模拟实现
- 我们自定义一个MVVM的类,来模拟new Vue的过程。
let vm = new MVVM({
el: "#app",
data: {
a: 1,
b: {
c: 2
},
c: "<p>56165</p>"
}
});
- MVVM
class MVVM {
constructor(options) {
this.$el = options.el; // 挂载点
this.$data = options.data; // 数据
new Compile(this.$el, this.$data); // 解析模板
}
}
- 解析模板
- 查看传入的el是元素还是字符串
- 如果是字符串,根据class或id查找元素
- 如果是元素直接使用
- 如果元素存在,将元素加入到内存中,解析模板
- 查看传入的el是元素还是字符串
class Compile {
constructor(el, data) {
this.$el = this.isElement(el) ? el : document.querySelector(el);
this.$data = data;
if (this.$el) {
// 第一步:加入内存
let fragment = this.toFragment(this.$el);
// 第二步:解析指令
this.compileDirect(fragment);
// 第三步:加入到DOM中
this.$el.appendChild(fragment);
}
}
}
查看是否是元素
isElement(node) {
// 1代表是元素
return node.nodeType === 1;
}
- 加入到内存
toFragment(element) {
// 创建文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
// 逐个获取第一个元素加入到碎片中,直到文档中没有子元素
while (firstChild = element.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
效果

- 解析指令
- 获取子节点
- 如果子节点是元素,解析元素,递归遍历子元素。
- 如果是文本,解析文本。
compileDirect(el) {
let children = el.childNodes;
[...children].forEach(child => {
if (this.isElement(child)) {
// 解析元素
this.compileNode(child);
// 遍历子元素
this.compileDirect(child);
} else {
// 解析文本
this.compileText(child);
}
});
}
2.1 创建工具类
compileUtil = {
// v-text || 文本
text(node, value) {
node.textContent = value;
},
// v-model
model(node, value) {
node.value = value;
},
// v-html
html(node, value) {
node.innerHTML = value;
}
}
2.2 解析文本
compileText(node) {
// 获取文本内容,{{ a }}
let text = node.textContent;
if (text) {
// 正则匹配, 是否包含指令,{{ a }}
let value = text.match(/{{([^}])+}}/g);
if (value) {
// 获取 {{ a }} 里面的数据: a
value = value[0].replace(/{{([^}]+)}}/g, '$1').trim();
compileUtil["text"](node, this.getValue(value));
}
}
}
2.2.1 获取文本指令的值
- 如果为级联属性,a.b.c需要一层一层的获取值
getValue(value) {
value = value.split(".");
// 第一次为 this.$data["a"]
// 第二次为 this.$data["a"]["b"]
// 第三次为 this.$data["a"]["b"]["c"]
return value.reduce((prevRes, next) => {
return prevRes[next];
}, this.$data);
}
2.3 解析元素属性指令的值
- 获取元素的所有属性
- 如果有遍历每一个属性
- 获取到属性的名字 如: class、v-model(这里没有实现v-bind以及缩写的形式)
- 根据名字获取对应处理的函数,获取对应的值

compileNode(node, data) {
let attrs = [...node.attributes];
if (attrs.length > 0) {
attrs.forEach(attr => {
// v-model → model
// v-text → text
// v-html → html
let type = attr.name.slice(2);
// 如果有对应的函数则处理
compileUtil[type] && compileUtil[type](node, this.getValue(attr.value) || attr.value);
});
}
}
- 插入到文档中
this.$el.appendChild(fragment);

总结
- 简单的实现了一个Compile的过程,知识是实现了一些简单的指令解析,还没有实现v-bind、v-on以及它们的缩写形式:、@。
- 重点再于理解,不只是知道和会用,还要学会灵活的运用以及知道他们简单的原理并能加以实现。
- 如果有兴趣的可以看一下我的另两篇文章