// 模板内容
<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
那他是如何进行渲染的呢,咱们带着问题来进入正题。node
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来对编译模板。git
新建一个compile.js的文件,并建立Compile类github
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是不是元素节点,若是是元素节点使用,若是是选择器,就获取元素后进行使用。app
// isElememtNode
isElememtNode(node) {
return node.nodeType === 1;
}
复制代码
node2fragment(el) {
// 建立文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild) {
// 把dom元素移入到fragment
fragment.appendChild(firstChild);
}
return fragment;
}
复制代码
这样咱们就获得了fragment,接下来的处理,咱们只须要对fragment进行处理便可。dom
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。mvvm
咱们再分别来看一下compileElement和compileText两个方法函数
// 编译文本节点
compileText(node) {
let expr = node.textContent;
// 匹配开头是{{结尾是}}而且中间不存在}的值
let reg = /\{\{([^}]+)\}\}/g;
if (reg.test(expr)) {
CompileUtil['text'](node, this.vm, expr);
}
}
复制代码
其中用到的正则:ui
/\{\{([^}]+)\}\}/g;
复制代码
若是对这个正则不理解,咱们能够配合图来理解一下
他实现的功能就是匹配开头是 {{ 结尾是 }} 而且中间不存在 } 的字符串模板。
获得字符串模板以后咱们就能够vm实例中取到对应的值,具体的处理,咱们分离到CompileUtil中来实现。
若是是元素节点,咱们须要考虑的就是其存在指令的状况(本篇文章只讲述v-model的状况)
咱们分为三步来实现该功能
// 编译元素节点
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这个辅助类,咱们能够来看一下其代码实现。
咱们先来看对于text的处理。
通过以上的处理,咱们会拿到相似于{{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,到这里,咱们还须要如下几步:
为了实现以上需求,咱们给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指令的编译都已经完成。
this.el.appendChild(fragment);
复制代码
到这里,一个基础的编译环节就宣告完成,打开页面就能获得期待的渲染结果了👏👏👏
斗胆发文,欢迎吐槽和指正。
附上完整代码示例,期待与您共同进步:github.com/Ljhhhhhh/mv…