model-view-viewmodel,通过数据劫持+发布者订阅模式来实现
在model中定义数据修改和操作的业务逻辑
在view表示ui组件,负责将数据模型转化为ui展示出来,做的是数据绑定声明,指令的声明,事件绑定的声明
viewmodel是一个同步view和model的对象,view和model没有直接的联系,通过viewmodel来进行交互的
a. MVVM是数据驱动,MVC是dom驱动
b.MVVM优点在于不用操作大量的DOM,不用关注view和model之间的关系,而MVC需要
1、低耦合性 view 和 model 之间没有直接的关系,通过 viewModel 来完成数据双向绑定。
2、可复用性 组件是可以复用的。可以把一些数据逻辑放到一个 viewModel 中,让很多 view 来重用。
3、独立开发 开发人员专注于 viewModel ,设计人员专注于view。4、可测试性 ViewModel 的存在可以帮助开发者更好地编写测试代码
a, bug很难调试,因为数据双向绑定,所以问题可能在view中,也可能在model,找到其原始定位难度高
b.一个模块中的model内存可能很大,长期保存会影响性能
c. 对于大型的图形应用程序,视图状态越多,viewmodel的维护成本较高
MVVM核心是数据劫持,数据代理,数据编译,和‘发布订阅者模式’
a.数据劫持:就是给对象属性添加get,set钩子函数
1、观察对象,给对象增加 Object.defineProperty
2、vue的特点就是新增不存在的属性不会给该属性添加 get 、 set 钩子函数。
3、深度响应。循环递归遍历 data 的属性,给属性添加 get , set 钩子函数。
4、每次赋予一个新对象时(即调用 set 钩子函数时),会给这个新对象进行数据劫持( defineProperty )。
//通过set、get钩子函数进行数据劫持
function defineReactive(data){
Object.keys(data).forEach(key=>{
const dep=new Dep();
let val=data[key];
this.observe(val);//深层次的监听
Object.defineProperty(data,key,{
get(){
//添加订阅者watcher(为每一个数据属性添加订阅者,以便实时监听数据属性的变化——订阅)
Dep.target&&dep.addSub(Dep.target);
//返回初始值
return val;
},set(newVal){
if(val!==newVal){
val=newVal;
//通知订阅者,数据变化了(发布)
dep.notify();
return newVal;
}
}
})
})
}
b、数据代理
将 data , methods , compted
上的数据挂载到vm
实例上。让我们不用每次获取数据时,都通过 mvvm._data.a.b 这种方式,而可以直接通过 mvvm.b.a 来获取。
class MVVM{
constructor(options){
this.$options=options;
this.$data=options.data;
this.$el=options.el;
this.$computed=options.computed;
this.$methods=options.methods;
//劫持数据,监听数据的变化
new Observer(this.$data);
//将数据挂载到vm实例上
this._proxy(this.$data);
//将方法也挂载到vm上
this._proxy(this.$methods);
//将数据属性挂载到vm实例上
Object.keys(this.$computed).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return this.$computed[key].call(this);//将vm传入computed中
}
})
})
//编译数据
new Compile(this.$el,this)
};
//私有方法,用于数据劫持
_proxy(data){
Object.keys(data).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return data[key]
}
})
})
}
}
c.数据编译
把 {{}} , v-model , v-html , v-on
,里面的对应的变量用data里面的数据进行替换
class Compile{
constructor(el,vm){
this.el=this.isElementNode(el)?el:document.querySelector(el);
this.vm=vm;
let fragment=this.nodeToFragment(this.el);
//编译节点
this.compile(fragment);
//将编译后的代码添加到页面
this.el.appendChild(fragment);
};
//核心编译方法
compile(node){
const childNodes=node.childNodes;
[...childNodes].forEach(child=>{
if(this.isElementNode(child)){
this.compileElementNode(child);
//如果是元素节点就还得递归编译
this.compile(child);
}else{
this.compileTextNode(child);
}
})
};
//编译元素节点
compileElementNode(node){
const attrs=node.attributes;
[...attrs].forEach(attr=>{
//attr是一个对象
let {name,value:expr}=attr;
if(this.isDirective(name)){
//只考虑到v-html和v-model的情况
let [,directive]=name.split("-");
//考虑v-on:click的情况
let [directiveName,eventName]=directive.split(":");
//调用不同的指令来进行编译
CompileUtil[directiveName](node,this.vm,expr,eventName);
}
})
};
//编译文本节点
compileTextNode(node){
const textContent=node.textContent;
if(/\{\{(.+?)\}\}/.test(textContent)){
CompileUtil["text"](node,this.vm,textContent)
}
};
//将元素节点转化为文档碎片
nodeToFragment(node){
//将元素节点缓存起来,统一编译完后再拿出来进行替换
let fragment=document.createDocumentFragment();
let firstChild;
while(firstChild=node.firstChild){
fragment.appendChild(firstChild);
}
return fragment;
};
//判断是否是元素节点
isElementNode(node){
return node.nodeType===1;
};
//判断是否是指令
isDirective(attr){
return attr.includes("v-");
}
}
//存放编译方法的对象
CompileUtil={
//根据data中的属性获取值,触发观察者的get钩子
getVal(vm,expr){
const data= expr.split(".").reduce((initData,curProp)=>{
//会触发观察者的get钩子
return initData[curProp];
},vm)
return data;
},
//触发观察者的set钩子
setVal(vm,expr,value){
expr.split(".").reduce((initData,curProp,index,arr)=>{
if(index===arr.length-1){
initData[curProp]=value;
return;
}
return initData[curProp];
},vm)
},
getContentValue(vm,expr){
const data= expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(vm,args[1]);
});
return data;
},
model(node,vm,expr){
const value=this.getVal(vm,expr);
const fn=this.updater["modelUpdater"];
fn(node,value);
//监听input的输入事件,实现数据响应式
node.addEventListener('input',e=>{
const value=e.target.value;
this.setVal(vm,expr,value);
})
//观察数据(expr)的变化,并将watcher添加到订阅者队列中
new Watcher(vm,expr,newVal=>{
fn(node,newVal);
});
},
text(node,vm,expr){
const fn=this.updater["textUpdater"];
//将{{person.name}}中的person.james替换成james
const content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
//观察数据的变化
new Watcher(vm,args[1],()=>{
// this.getContentValue(vm,expr)获取textContent被编译后的值
fn(node,this.getContentValue(vm,expr))
})
return this.getVal(vm,args[1]);
})
fn(node,content);
},
html(node,vm,expr){
const value=this.getVal(vm,expr);
const fn=this.updater["htmlUpdater"];
fn(node,value);
new Watcher(vm,expr,newVal=>{
//数据改变后,再次替换数据
fn(node,newVal);
})
},
on(node,vm,expr,eventName){
node.addEventListener(eventName,e=>{
//调用call将vm实例(this)传到方法中去
vm[expr].call(vm,e);
})
},
updater:{
modelUpdater(node,value){
node.value=value
},
htmlUpdater(node,value){
node.innerHTML=value;
},
textUpdater(node,value){
node.textContent=value;
}
}
}
d.发布订阅
发布订阅主要靠的是数组关系,订阅就是放入函数(就是将订阅者添加到订阅队列中),发布就是让数组里的函数执行(在数据发生改变的时候,通知订阅者执行相应的操作)。消息的发布和订阅是在观察者的数据绑定中进行数据的——在get钩子函数被调用时进行数据的订阅(在数据编译时通过 new Watcher() 来对数据进行订阅
),在set钩子函数被调用时进行数据的发布
//消息管理者(发布者),在数据发生变化时,通知订阅者执行相应的操作
class Dep{
constructor(){
this.subs=[];
};
//订阅
addSub(watcher){
this.subs.push(watcher);
};
//发布
notify(){
this.subs.forEach(watcher=>watcher.update());
}
}
//订阅者,主要是观察数据的变化
class Watcher{
constructor(vm,expr,cb){
this.vm=vm;
this.expr=expr;
this.cb=cb;
this.oldValue=this.get();
};
get(){
Dep.target=this;
const value=CompileUtil.getVal(this.vm,this.expr);
Dep.target=null;
return value;
};
update(){
const newVal=CompileUtil.getVal(this.vm,this.expr);
if(this.oldValue!==newVal){
this.cb(newVal);
}
}
}
//观察者
class Observer{
constructor(data){
this.observe(data);
};
//使数据可响应
observe(data){
if(data&&typeof data==="object"){
this.defineReactive(data)
}
};
defineReactive(data){
Object.keys(data).forEach(key=>{
const dep=new Dep();
let val=data[key];
this.observe(val);//深层次的监听
Object.defineProperty(data,key,{
get(){
//添加订阅者watcher(为每一个数据属性添加订阅者,以便实时监听数据属性的变化——订阅)
Dep.target&&dep.addSub(Dep.target);
//返回初始值
return val;
},set(newVal){
if(val!==newVal){
val=newVal;
//通知订阅者,数据变化了(发布)
dep.notify();
return newVal;
}
}
})
})
}
}