刚写的原理,先放上来再说,后续补充解说
compile.js
/*
简单手写一把数据的编译,主要支持:
文本编译
输入框model编译
功能可以扩充,重在编译的思想。
*/
class Compile{
constructor(el,vm){
this.el = this.isElementNode(el) ? el : document.querySelector(el);//#app document.query。。。得区分开来是哪个
this.vm = vm;
if(this.el){//如果这个元素能获取到,我们才开始编译
/*
1. 编译就要先获取dom,如何获取?
方案一(劣):一个个DOM获取,然后编译一个,放到页面上去,这样频繁操作dom性能不好,不建议
方案二(优):先把这些真实的Dom移入到内存中 利用文档碎片fragment
2. 编译=>提取想要的元素节点 v-model 和 文本节点{{}}
3. 把编译好的fragment再塞回到页面里去
*/
//1. 将真实dom放到内存中
let fragment = this.node2fragment(this.el);
//2. 编译文档碎片,提取想要的元素节点 v-model 和 文本节点{{}}
this.compile(fragment);
//3. 编译完,塞回去
this.el.appendChild(fragment);
}
}
/*专门写一些辅助方法*/
//判断el是否为dom
isElementNode(node){
/*判断是否为元素节点*/
return node.nodeType === 1;
}
//判断是否为指令
isDirective(name){
return name.includes("v-");
}
/*专门写一些核心方法*/
//编译元素
compileElement(node){//假设编译带v-model的
//获取节点的属性
let attrs = node.attributes;//返回的attrs是一个map映射,类数组,可循环
Array.from(attrs).forEach(attr => {
//console.log(attr);//得到的是type="text"等属性和属性值的对,是对象类型,key为"name"
//判断属性名字是否包含v-
let attrName = attr.name;
if(this.isDirective(attrName)){
//取到对应的值,放到节点的中
let expr = attr.value;
let type = attrName.slice(2);
//找到节点node,去数据this.vm.$data中把对应的值取出来,放到对应的节点中
CompileUtil[type](node,this.vm,expr);
}
})
}
//编译文本
compileText(node){
//带{{}},但是不能是{{asas}sas}}
let expr = node.textContent;//取出文本中的内容
let reg = /\{\{([^}]+)\}\}/g;//要考虑到多个{{}}组成的一个文本
if(reg.test(expr)){
//node this.vm.$data
CompileUtil['text'](node,this.vm,expr);
}else{
}
}
//编译碎片
compile(fragment){
//拿到所有的节点,包括子节点
//需要递归
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if(this.isElementNode(node)){
//如果是元素节点,还需要继续深入的检查子节点是元素还是文本
//这里需要编译元素
this.compileElement(node);
this.compile(node);
}else {
//文本节点,看看是否有大括号{}
//这里需要编译文本
this.compileText(node);
}
})
}
//把真实dom放入内存中
node2fragment(el){
//先创建一个内存中的文档碎片(不是真实的),用于存放拷贝过来的真实DOM,变成内存中的DOM节点
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild){
fragment.appendChild(firstChild);
}
return fragment;
}
}
//专门用于编译的对象,假如要编译html,那只要在这个对象里加方法就可以了
CompileUtil = {
text(node,vm,expr){//文本处理,expr可能是{{s}} {{sc}} {{sds}}
let updateFn = this.update['textUpdater'];
let value = this.getTextVal(expr,vm);
expr.replace(/\{\{([^}]+)\}\}/g,(...arguments) => {
new Watcher(vm,arguments[1],(newValue) => {
//如果数据变化了,文本节点需要重新获取依赖的数据,更新文本中的内容
updateFn && updateFn(node,this.getTextVal(expr,vm));
})
})
updateFn && updateFn(node,value);
},
model(node,vm,expr){//输入框的处理
let updateFn = this.update['modelUpdater'];
//let data = vm.$data[expr];//这样不行,因为expr可能是"message.a",也就是说可能是获取data中message下的属性a
//所以,针对以上情况,我们需要根据“.”来split,然后获取到对应的属性值
//这里应该加一个监控,数据变化了,就应该调用下watch的callback
new Watcher(vm,expr,(newVal) => {
//当值变化后,会调用cb,将新值传递过来
updateFn && updateFn(node,this.getVal(expr,vm));
});
node.addEventListener('input',(e) => {
let newValue = e.target.value;
this.setVal(expr,vm,newValue);
})
updateFn && updateFn(node,this.getVal(expr,vm));
},
update:{
//文本更新
textUpdater(node,value){
node.textContent = value;
},
//输入框更新
modelUpdater(node,value){
node.value = value;
}
},
getVal(expr,vm){//获取实例上对应的数据
//reduce
expr = expr.split('.');//[message,a,b,c]
return expr.reduce((prev,next) => {//vm.$data.message
return prev[next];
},vm.$data)
},
getTextVal(expr,vm){
return expr.replace(/\{\{([^}]+)\}\}/g,(...arguments) => {
return this.getVal(arguments[1],vm);
})
},
setVal(expr,vm,value){//expr可能为[message,a]
expr= expr.split('.');
expr.reduce((prev,next,currentIndex) => {
if(currentIndex === expr.length - 1){
return prev[next] = value;
}
return prev[next];
},vm.$data);
}
}