MVVM模式是Model-View-ViewModel的简写,即模型-视图-视图模型。【模型】指的是数据层。【视图】指的是视图层所看到的页面。【视图模型】mvvm模式的核心,它是连接view和model的桥梁。
数据层和视图层直接是不能直接通信的,那么模型(数据层)如何转换成视图:通过绑定数据。视图如何转换成数据:通过DOM事件监听。当这个两个都实现了,就完成了数据的双向绑定。MVVM模式Model和View是通过ViewModel作为桥梁进行通信的。
Vue实现数据响应式变化其原理就是MVVM原理,通过定义一个Watcher类(观察者),Dep类(被观察者)作为Model(数据层)和View(视图层)之间的桥梁。Vue通过模板编译(Compiler类)来实现数据层向视图层的转换,通过数据劫持(Observe类)来实现对数据的监听。通过在模板编译的过程中new 一个watcher对象,监控数据是否发生变化,如果数据发生变化则继续更新视图。通过数据劫持的过程中,通过收集每个唯一的watcher对象,在用户重新设置数据的时候,将唯一的watcher对象一一触发更新。则代码的实现过程如下:
1、实现一个模板编译Compiler类,主要的功能是生成一个虚拟DOM(提高其性能),通过更新虚拟DOM来间接更新真实DOM以及具体如何编译模板,将节点中的内容通过真实的数据进行替换。
class Compiler{
constructor(el,vm){
this.vm=vm;
this.el=this.isElementNode(el)?el:document.querySelector(el);
//把当前节点中的元素获取到放到内存中
let fragment=this.node2fragment(this.el)
//把节点中的内容进行替换
//编译模板 用数据编译
this.compile(fragment)
//把这个内容在塞到页面中
this.$el.appendChild(fragment);
}
isDirective(attrName){
return attrName.startWith('v-');
}
compileElement(node){
let attr=node.attributes;//类数组
[...attr].forEach(atr=>{
let {name,value:expr}=atr;
if(this.isDirective(name)){
console.log(node) //指令元素
let[,directive]= name.split('-');
let [directiveName,eventName]=directive.split('.'); //处理v-on:click指令
CompilerUtils[directiveName](node,expr,)
CompilerUtils[directive](node,expr,this.vm,eventName)
}
})
}
compileText(node){
let content=node.textcontent; //节点的文本
if(/\{\{(.+?)\}\}/.test(content)){
console.log(content)//找到所有文本
CompilerUtils['text'](node,content,this.vm)
}
}
//用来编译内存中的dom节点
compile(node){
let childNodes=node.childNodes; //dom的第一层
[...childNodes].forEach(child=>{
if(this.isElementNode(child)){
console.log('element');
this.compileElement(child);
//如果是元素的话,需要遍历子节点
this.compile(child);
}else{
console.log('text');
this.compileText(child)
}
})
}
node2fragment(node){
//把所有的节点都拿到,创建一个文档碎片。
let fragment=document.createDocumentFragment();
let firstChild;
while(firstChild=node.firstChild){
fragment.appendChild(firstChild);
}
return fragment;
}
isElementNode(node){
return node.nodeType===1
}
}
//处理不同指令不同的功能调用不同的处理方式
CompilerUtils={
//获取值
getValue(vm,expr){
let value=expr.split('.').reduce((data,current)=>{
return data[current];
},vm.$data)
return value;
},
//设置值
setValue(vm,expr,value){
return expr.split('.').reduce((data,current,index,arr)=>{
if(index===arr.length-1){
return data[current]=value;
}
return data[current];
},vm.$data)
},
model(node,expr,vm){
let fn=this.updater['modelUpdater']
let value=this.getValue(vm,expr);
new Watcher(vm,expr,(newValue)=>{
fn(node,newValue);
})
node.addEventListener('input',(e)=>{
let value=e.target.value;
this.setValue(vm,expr,value);
})
fn(node,value);
},
on(node,expr,vm,eventName){
node.addEventListener(eventName,(e)=>{
return vm[expr].call(vm,e);
})
},
html(){
},
getContentValue(vm,expr){
return expr.replace(/\{\{(.+?)\}\}/g,(...agrs)=>{
return this.getValue(vm,args[1]);
})
},
text(node,expr,vm){ //expr {{a}} {{b}} {{c}}
let content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
new Watcher(vm,args[1],()=>{
let value=this.getContentValue(vm,expr); //返回了一个全的字符串
fn(node,value)
})
return this.getValue(vm,args[1]);
})
let fn=this.updater['textUpdater'];
fn(node,content);
},
updater:{
modelUpdater(node,value){
node.value=value;
},
htmlUpdater(node,value){
node.innerHTML=value;
},
textUpdater(node,value){
node.textContent=value; //将值放在节点的文本内容上
}
}
}
2、实现一个Observe类,将对象通过Object.defineProperty(obj,key,value)重新定义来劫持数据做一些其他的事情,数组通过重写数组的方法。
//vue内部是如何实现响应式数据变化的,响应式数据变化有什么特点
//实现响应式数据变化对象是通过其核心defineProperty()实现,数组是通过数组的方法进行重写。
// 特点: 使用对象的时候 必须先声明属性 ,这个属性才是响应式的
// 1.增加不存在的属性 不能更新视图 (vm.$set)
// 2。默认会递归增加 getter和setter
// 3.数组里套对象 对象是支持响应式变化的,如果是常量则没有效果
// 4.修改数组索引和长度 是不会导致视图更新的
// 5.如果新增的数据 vue中也会帮你监控(对象类型)
let arrayProto=Array.prototype; //先存一份原生的原型
let proto=Object.create(Array.prototype); //复制一份一模一样原生的原型。
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(item=>{
proto[item]=function(...ary){
let insert; //数组新增的元素也要进行观察
switch(item){
case 'push':insert=ary;
case 'unshift':insert=ary;
case 'splice':insert=ary.slice(2);
default:
break;
}
observer(insert);
console.log('视图更新')
return arrayProto[item].call(arrayProto,...ary);
}
})
function observerArray(array){
for(let i=0,len=array.length;i<len;i++){
let item=array[i];
observer(item);
}
}
function observer(obj){
if(typeof obj!=='object' || obj===null){
return obj;
}
if(Array.isArray(obj)){
Object.setPrototypeOf(obj,proto)
observerArray(obj)
}else{
for(let key in obj){
defineReactive(key,obj[key],obj)
}
}
}
function defineReactive(key,value,obj){
observer(value); //通过递归的方式观察对象的值是否也是对象,如果是对象,更改数据会发生视图层的变化
Object.defineProperty(obj,key,{
get:function(){
return value;
},
set:function(newValue){
if(newValue!==value){
value=newValue;
observer(newValue); //新值也要观测
console.log('视图发生更新')
}
}
})
}
3、通过观察者和被观察者模式实现Dep类(实现对watcher对象的订阅和发布watcher对象的更新方法)和Watcher类(实现对新老值的比较触发值变化之后的回调函数)将Compiler类和Observe类进行连接,给Observe添加被观察者对象,Compiler类添加观察着对象。当编译模板时,new Watcher生成一个对象的同时触发Watcher类中获取老数据的get函数,当获取数据时则又触发了通过Object.defineProperty()的get()方法,在这个方法当中new Dep 生成一个dep对象,通过dep对象收集watcher,最后在设置数据set()方法中发布watcher。
class Dep{
constructor(){
this.subs=[]; //存放watcher
}
//订阅
addSubs(watcher){
this.subs.push(watcher)
}
//发布
notify(){
this.subs.forEach(watcher=>{
watcher.updater();
})
}
}
class Watcher{
constructor(vm,expr,cb){
this.vm=vm;
this.expr=expr;
this.cb=cb;
this.oldval=this.get()
}
get(){
Dep.target=this;
let value=CompilerUtils.getValue(this.vm,this.expr);
Dep.target=null;
return value;
}
updater(){ //更新操作 数据变化后 会调用观察者的update方法
let newValue=CompilerUtils.getValue(this.vm,this.expr);
if(newValue!==this.oldval){
this.cb(newValue);
}
}
}