文章目录
#1.vue的强大之处不必细说,vue的核心v-model的实现原理,网上都有很多。但是真正自己实现双向绑定,mvvm的源码却几乎没见过。
#1.2本人根据源码的解读,理解,以及借鉴网上的视频教程,手写一份mvvm,希望能帮助更多的vue学习者。
#2.先看成果
#3.实现原理
#3.1.数据劫持(observe)+模板编译(compile)+数据监听(watch)+发布订阅(Dep)
#4.源码
#4.1mvvm(架构)
class MVVM{
constructor(optins){
//1.先将传入的参数进行挂载
this.$el=optins.el;
this.$data=optins.data;
//2.进行模板编译
if(this.$el){
//先进行数据劫持
new Observe(this.$data);
new Compile(this.$el,this);
}
}
}
#4.2模板编译(conpile)
class Compile{
constructor(el,vm) { //el挂载名称,vm 是 WrM实例
this.el=this.isElememntNode(el)?el:document.querySelector(el); //进行判断,挂载节点
this.vm=vm;
if(this.el){ //
//1.首先取出节点,获取到节点
let fragment=this.node2fragment(this.el)
//2.编译=>提取想要的元素节点 w-model和 文本节点 {{}}
this.compile(fragment);
//2.将节点放入到文档碎片中(内存) fragment
//3.将编译好的fragment放回到真实dom中
this.el.appendChild(fragment);
}
}
/* 辅助方法*/
//1.判断是不是节点
isElememntNode(node){
return node.nodeType===1;
}
//判断元素节点是否包含w-指令
isDirective(attrName){
return attrName.includes('w-');
}
/*核心方法 */
//1.将dom节点移入内存中
node2fragment(el){ //将el中的内容全部放到内存中
let fragment=document.createDocumentFragment();//开辟内存存储空间
let firstChild; //定义为第一个节点
while(firstChild= el.firstChild){ //每次将取到的第一个节点放入内存
fragment.appendChild(firstChild)
}
return fragment;
}
//2.进行编译
compile(fragment){
//递归
let nodeChild=fragment.childNodes;
Array.from(nodeChild).forEach(node=>{
if(this.isElememntNode(node)){ //如果是元素,证明里面可能嵌套子节点,递归
//元素节点
//编译元素
this.compileElememnt(node);
this.compile(node);
}
else{
//文本节点
//编译文本
this.compileText(node);
}
})
}
//2.1编译元素
compileElememnt(node){
//带有 v-model 等指令的
let attrs=node.attributes; //取出当前元素的属性
Array.from(attrs).forEach(attr=>{
//判断属性名字是否包含v-
let attrName=attr.name;
if(this.isDirective(attrName)){
//取值,放到节点的值中
let expr=attr.value;
//
let type=attrName.slice(2);
// node this.vm.$data expr
compileUtile[type](node,this.vm,expr);
}
})
}
//2.2文本编译
compileText(node){
//有{{}}
let expr = node.textContent; //取文本中的节点
let reg= /\{\{([^}]+)\}\}/g //正则表达式,匹配{{}}中的内容
if(reg.test(expr)){
// node this.vm.$data expr
compileUtile['text'](node,this.vm,expr);
}
}
}
//编译工具 compileUtile
compileUtile={
//取值操作
getVal(vm,expr){
expr=expr.split('.'); //分割成数组
return expr.reduce((prev,next)=>{ //收敛
return prev[next];
},vm.$data)
},
getTextVal(vm,expr){
return expr=expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{
return this.getVal(vm,arguments[1]);
})
},
text(node,vm,expr){ //文本处理
let ex=expr;
let updatafn=this.updata['textUpdater'];
let value=this.getTextVal(vm,expr);
expr=expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{
new watch(vm,arguments[1],()=>{
updatafn && updatafn(node,this.getTextVal(vm,ex));
});
})
updatafn && updatafn(node,value);
},
set(vm,expr,value){
expr= expr.split('.');
return expr.reduce((prev,next,currnindex)=>{
if(currnindex===expr.length-1){
return prev[next]=value;
}
return prev[next];
},vm.$data)
},
model(node,vm,expr){ //输入框处理
let updatafn=this.updata['modelUpdater'];
new watch(vm,expr,(newvalue)=>{
updatafn && updatafn(node,this.getVal(vm,expr));
})
node.addEventListener('input',(e)=>{
let newValue=e.target.value;
this.set(vm,expr,newValue);
})
updatafn && updatafn(node,this.getVal(vm,expr));
},
updata:{
//文本更新 {{}}
textUpdater(node,value){
node.textContent=value;
},
//输入框更新 w-model
modelUpdater(node,value){
node.value=value;
}
}
}
#4.3数据劫持(observe)+发布订阅(dep)
class Observe{
constructor(data) {
//开始数据劫持
this.observe(data);
}
observe(data){
if(!data || typeof data !=='object')//判断是否为对象类型
{
return;
}
else{
//开始劫持
Object.keys(data).forEach(key=>{
this.definReacative(data,key,data[key]);
this.observe(data[key]); //深度递归
});
}
}
//定义响应式
definReacative(obj,key,value){
let that=this;
let dep=new Dep();
Object.defineProperty( obj,key,{
enumerable:true, //是否可枚举
configurable:true,
get(){
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue){
if(newValue!=value){
that.observe(newValue);
value=newValue;
dep.notify();
}
}
});
}
}
//发布订阅
class Dep{
constructor(){
this.subs=[] //设置订阅数组
}
addSub(watch){
this.subs.push(watch); //添加到数组中
}
notify(){
this.subs.forEach(watch=>watch.updata()); //发布
}
}
#4.4mvvm架构
class MVVM{
constructor(optins){
//1.先将传入的参数进行挂载
this.$el=optins.el;
this.$data=optins.data;
//2.进行模板编译
if(this.$el){
//先进行数据劫持
new Observe(this.$data);
new Compile(this.$el,this);
}
}
}
#5.使用方法
引入上述4个文件
w-model (数据的双向绑定) {{}} (插值表达式)