// 模板内容
<div id="app">
<input type="text" v-model="message">
{{message}}
<div>{{a.b}}</div>
</div>
// vue脚本
let vm = new Vue({
el: '#app',
data: {
message: '我是message',
a: {
b: '我的a.b'
}
}
})
复制代码
看到上面的代码,使用过vue的同学能知道页面的渲染结果会如下图所示:
那他是如何进行渲染的呢,我们带着问题来进入正题。
首先新建Vue.js并创建一个名为Vue的类:
class Vue {
constructor(options) {
// 挂载可用数据到实例上
this.$el = options.el;
this.$data = options.data;
// 如果含有模板就去编译
if (this.$el) {
// 用数据和元素进行编译
new Compile(this.$el, this);
}
}
}
复制代码
以上代码就是对new Vue时传递的参数el和data进行存储,再利用Compile来对编译模板。
Compile类对模板进行处理:
新建一个compile.js的文件,并创建Compile类
class Compile{
constructor(el ,vm) {
this.el = this.isElememtNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
// 把需要操作的dom先放到内存中
let fragment = this.node2fragment(this.el);
// 编译:提取元素节点的v-model和文本节点{{}}
this.compile(fragment);
// 把编译完成的元素放到页面中
this.el.appendChild(fragment);
}
}
}
复制代码
由于Vue中的el是可以传递选择器和元素节点的,我们这里也对el做了相应的处理。
判断用户传递的el是否是元素节点,如果是元素节点使用,如果是选择器,就获取元素后进行使用。
// isElememtNode
isElememtNode(node) {
return node.nodeType === 1;
}
复制代码
获取跟元素节点后,利用node2fragment函数把dom元素放入内存中处理:
node2fragment(el) {
// 创建文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild) {
// 把dom元素移入到fragment
fragment.appendChild(firstChild);
}
return fragment;
}
复制代码
这样我们就得到了fragment,接下来的处理,我们只需要对fragment进行处理即可。
拿到了文档碎片fragment,我们就可以开始编写Compile核心函数了
compile(fragment) {
// 获取fragment的所有子元素
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElememtNode(node)) {
// 编译元素
this.compileElement(node);
// 递归执行
this.compile(node);
} else {
this.compileText(node);
}
})
}
复制代码
获取所有子元素后,分别针对是元素节点和文本节点的情况进行处理,需要指出的一点就是,元素节点内部可能还有子元素, 所以我们以当前子节点为参数递归执行compile。
我们再分别来看一下compileElement和compileText两个方法
compileText
// 编译文本节点
compileText(node) {
let expr = node.textContent;
// 匹配开头是{{结尾是}}并且中间不存在}的值
let reg = /\{\{([^}]+)\}\}/g;
if (reg.test(expr)) {
CompileUtil['text'](node, this.vm, expr);
}
}
复制代码
其中用到的正则:
/\{\{([^}]+)\}\}/g;
复制代码
如果对这个正则不理解,我们可以配合图来理解一下
他实现的功能就是匹配开头是 {{ 结尾是 }} 并且中间不存在 } 的字符串模板。
得到字符串模板之后我们就可以vm实例中取到对应的值,具体的处理,我们分离到CompileUtil中来实现。
compileElement
如果是元素节点,我们需要考虑的就是其存在指令的情况(本篇文章只讲述v-model的情况)
我们分为三步来实现该功能
- 获取元素节点的属性集合
- 判断属性是否为指令(isDirective函数)
- 如果是指令,利用CompileUtil函数做对应处理。
// 编译元素节点
compileElement(node) {
let attrs = node.attributes; // 获取当前节点的属性
Array.from(attrs).forEach(attr => {
let attrName = attr.name;
// 如果是指令进行数据处理
if (this.isDirective(attrName)) {
let expr = attr.value;
let [,type] = attrName.split('-');
CompileUtil[type](node, this.vm, expr)
}
})
}
// 如果是v-开头,我们就认为他是指令
isDirective(name) {
return name.startsWith('v-');
}
复制代码
以上compileText和compileElement两个方法中,具体的处理方式都使用到了CompileUtil这个辅助类,我们可以来看一下其代码实现。
CompileUtil
我们先来看对于text的处理。
经过以上的处理,我们会拿到类似于{{XXX}}的字符串,有了这个字符串,我们还需要下面几步:
- 得到{{xxx}}中的xxx
- 寻找vm.$data中xxx对应的值
- 得到对应值后,更新对应节点的文本内容
上面需要处理的一个难点是:我们的需要的值可能是对象中的对象,类似于{{a.b.c}},解决方案为:先把字符串分隔成数组,再使用reduce每次都取到下一个key,最后利用key取到对应对象的值。
// 编译所需的辅助方法
CompileUtil = {
getVal(vm, expr) { // 获取实例上对应的数据
expr = expr.split('.');
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
},
getTextVal(expr, vm) {
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
// expr: {{XXX}}
// arguments[1]是XXX
return this.getVal(vm, arguments[1]);
});
},
text(node, vm, expr) { // 文本处理
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(expr, vm);
updateFn && updateFn(node, value);
},
updater: {
textUpdater(node, value) {
node.textContent = value;
}
}
}
复制代码
处理完了text,再来看如何处理指令
在上面的compileElement方法中,我们判断了节点属性是否是指令,如果是指令我们就拿到具体的指令,例如v-model我们就拿到model,到这里,我们还需要以下几步:
- 获取到指令所对应的key,例如v-model=“message"中的message
- 更新节点的value值为vm.
data.message
- 设置节点的value值为对应的值
为了实现以上需求,我们给CompileUtil新增model方法
model(node, vm ,expr) { // v-model处理
let updateFn = this.updater['modelUpdater'];
updateFn && updateFn(node, this.getVal(vm, expr));
},
复制代码
对应的modelUpdater:
modelUpdater(node, value) {
node.value = value;
}
复制代码
完整的CompileUtil代码如下
// 编译所需的辅助方法
CompileUtil = {
getVal(vm, expr) { // 获取实例上对应的数据
expr = expr.split('.');
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
},
getTextVal(expr, vm) {
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1]);
});
},
text(node, vm, expr) { // 文本处理
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(expr, vm);
updateFn && updateFn(node, value);
},
model(node, vm ,expr) { // v-model处理
let updateFn = this.updater['modelUpdater'];
updateFn && updateFn(node, this.getVal(vm, expr));
},
updater: {
textUpdater(node, value) {
node.textContent = value;
},
modelUpdater(node, value) {
node.value = value;
}
}
}
复制代码
到这里,文本节点和v-model指令的编译都已经完成。
最后一步,就是把文档碎片fragment放回到根节点中去
this.el.appendChild(fragment);
复制代码
到这里,一个基础的编译环节就宣告完成,打开页面就能得到期待的渲染结果了???
斗胆发文,欢迎吐槽和指正。
附上完整代码示例,期待与您共同进步:github.com/Ljhhhhhh/mv…