mvvm的概念、原理及实现
代码实现来源于珠峰公开课 mvvm 原理的讲解。此文在此记录一下,通过手写几遍代码加深一下自己对 mvvm 理解。
1、MVVM的概念
model-view-viewModel,通过数据劫持+发布订阅模式来实现。
mvvm是一种设计思想。Model代表数据模型,可以在model中定义数据修改和操作的业务逻辑;view表示ui组件,负责将数据模型转换为ui展现出来,它做的是数据绑定的声明、 指令的声明、 事件绑定的声明。;而viewModel是一个同步view和model的对象。在mvvm框架中,view和model之间没有直接的关系,它们是通过viewModel来进行交互的。mvvm不需要手动操作dom,只需要关注业务逻辑就可以了。
mvvm和mvc的区别在于:mvvm是数据驱动的,而MVC是dom驱动的。mvvm的优点在于不用操作大量的dom,不需要关注model和view之间的关系,而MVC需要在model发生改变时,需要手动的去更新view。大量操作dom使页面渲染性能降低,使加载速度变慢,影响用户体验。
2、mvvm的优点
- 1、低耦合性 view 和 model 之间没有直接的关系,通过 viewModel 来完成数据双向绑定。
- 2、可复用性 组件是可以复用的。可以把一些数据逻辑放到一个 viewModel 中,让很多 view 来重用。
- 3、独立开发 开发人员专注于 viewModel ,设计人员专注于view。
- 4、可测试性 ViewModel 的存在可以帮助开发者更好地编写测试代码。
3、mvvm的缺点
- 1、bug很难被调试,因为数据双向绑定,所以问题可能在 view 中,也可能在 model 中,要定位原始bug的位置比较难,同时view里面的代码没法调试,也添加了bug定位的难度。
- 2、一个大的模块中的 model 可能会很大,长期保存在内存中会影响性能。
- 3、对于大型的图形应用程序,视图状态越多, viewModel 的构建和维护的成本都会比较高。
4、mvvm的双向绑定原理
mvvm 的核心是数据劫持、数据代理、数据编译和"发布订阅模式"。
1、数据劫持——就是给对象属性添加get,set钩子函数。
- 1、观察对象,给对象增加 Object.defineProperty
- 2、vue的特点就是新增不存在的属性不会给该属性添加 get 、 set 钩子函数。
- 3、深度响应。循环递归遍历 data 的属性,给属性添加 get , set 钩子函数。
- 4、每次赋予一个新对象时(即调用 set 钩子函数时),会给这个新对象进行数据劫持( defineProperty )。
1 //通过set、get钩子函数进行数据劫持 2 function defineReactive(data){ 3 Object.keys(data).forEach(key=>{ 4 const dep=new Dep(); 5 let val=data[key]; 6 this.observe(val);//深层次的监听 7 Object.defineProperty(data,key,{ 8 get(){ 9 //添加订阅者watcher(为每一个数据属性添加订阅者,以便实时监听数据属性的变化——订阅) 10 Dep.target&&dep.addSub(Dep.target); 11 //返回初始值 12 return val; 13 },set(newVal){ 14 if(val!==newVal){ 15 val=newVal; 16 //通知订阅者,数据变化了(发布) 17 dep.notify(); 18 return newVal; 19 } 20 } 21 }) 22 }) 23 }
2、数据代理
将 data , methods , compted
上的数据挂载到vm
实例上。让我们不用每次获取数据时,都通过 mvvm._data.a.b 这种方式,而可以直接通过 mvvm.b.a 来获取。
1 class MVVM{ 2 constructor(options){ 3 this.$options=options; 4 this.$data=options.data; 5 this.$el=options.el; 6 this.$computed=options.computed; 7 this.$methods=options.methods; 8 //劫持数据,监听数据的变化 9 new Observer(this.$data); 10 //将数据挂载到vm实例上 11 this._proxy(this.$data); 12 //将方法也挂载到vm上 13 this._proxy(this.$methods); 14 //将数据属性挂载到vm实例上 15 Object.keys(this.$computed).forEach(key=>{ 16 Object.defineProperty(this,key,{ 17 get(){ 18 return this.$computed[key].call(this);//将vm传入computed中 19 } 20 }) 21 }) 22 //编译数据 23 new Compile(this.$el,this) 24 }; 25 //私有方法,用于数据劫持 26 _proxy(data){ 27 Object.keys(data).forEach(key=>{ 28 Object.defineProperty(this,key,{ 29 get(){ 30 return data[key] 31 } 32 }) 33 }) 34 35 } 36 }
3、数据编译
把 {{}} , v-model , v-html , v-on
,里面的对应的变量用data里面的数据进行替换。
1 class Compile{ 2 constructor(el,vm){ 3 this.el=this.isElementNode(el)?el:document.querySelector(el); 4 this.vm=vm; 5 let fragment=this.nodeToFragment(this.el); 6 //编译节点 7 this.compile(fragment); 8 //将编译后的代码添加到页面 9 this.el.appendChild(fragment); 10 }; 11 //核心编译方法 12 compile(node){ 13 const childNodes=node.childNodes; 14 [...childNodes].forEach(child=>{ 15 if(this.isElementNode(child)){ 16 this.compileElementNode(child); 17 //如果是元素节点就还得递归编译 18 this.compile(child); 19 }else{ 20 this.compileTextNode(child); 21 } 22 }) 23 24 }; 25 //编译元素节点 26 compileElementNode(node){ 27 const attrs=node.attributes; 28 [...attrs].forEach(attr=>{ 29 //attr是一个对象 30 let {name,value:expr}=attr; 31 if(this.isDirective(name)){ 32 //只考虑到v-html和v-model的情况 33 let [,directive]=name.split("-"); 34 //考虑v-on:click的情况 35 let [directiveName,eventName]=directive.split(":"); 36 //调用不同的指令来进行编译 37 CompileUtil[directiveName](node,this.vm,expr,eventName); 38 } 39 }) 40 }; 41 //编译文本节点 42 compileTextNode(node){ 43 const textContent=node.textContent; 44 if(/\{\{(.+?)\}\}/.test(textContent)){ 45 CompileUtil["text"](node,this.vm,textContent) 46 } 47 }; 48 //将元素节点转化为文档碎片 49 nodeToFragment(node){ 50 //将元素节点缓存起来,统一编译完后再拿出来进行替换 51 let fragment=document.createDocumentFragment(); 52 let firstChild; 53 while(firstChild=node.firstChild){ 54 fragment.appendChild(firstChild); 55 } 56 return fragment; 57 }; 58 //判断是否是元素节点 59 isElementNode(node){ 60 return node.nodeType===1; 61 }; 62 //判断是否是指令 63 isDirective(attr){ 64 return attr.includes("v-"); 65 } 66 } 67 //存放编译方法的对象 68 CompileUtil={ 69 //根据data中的属性获取值,触发观察者的get钩子 70 getVal(vm,expr){ 71 const data= expr.split(".").reduce((initData,curProp)=>{ 72 //会触发观察者的get钩子 73 return initData[curProp]; 74 },vm) 75 return data; 76 }, 77 //触发观察者的set钩子 78 setVal(vm,expr,value){ 79 expr.split(".").reduce((initData,curProp,index,arr)=>{ 80 if(index===arr.length-1){ 81 initData[curProp]=value; 82 return; 83 } 84 return initData[curProp]; 85 },vm) 86 }, 87 getContentValue(vm,expr){ 88 const data= expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ 89 return this.getVal(vm,args[1]); 90 }); 91 return data; 92 }, 93 model(node,vm,expr){ 94 const value=this.getVal(vm,expr); 95 const fn=this.updater["modelUpdater"]; 96 fn(node,value); 97 //监听input的输入事件,实现数据响应式 98 node.addEventListener('input',e=>{ 99 const value=e.target.value; 100 this.setVal(vm,expr,value); 101 }) 102 //观察数据(expr)的变化,并将watcher添加到订阅者队列中 103 new Watcher(vm,expr,newVal=>{ 104 fn(node,newVal); 105 }); 106 }, 107 text(node,vm,expr){ 108 const fn=this.updater["textUpdater"]; 109 //将{{person.name}}中的person.james替换成james 110 const content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ 111 //观察数据的变化 112 new Watcher(vm,args[1],()=>{ 113 // this.getContentValue(vm,expr)获取textContent被编译后的值 114 fn(node,this.getContentValue(vm,expr)) 115 116 }) 117 return this.getVal(vm,args[1]); 118 }) 119 fn(node,content); 120 }, 121 html(node,vm,expr){ 122 const value=this.getVal(vm,expr); 123 const fn=this.updater["htmlUpdater"]; 124 fn(node,value); 125 new Watcher(vm,expr,newVal=>{ 126 //数据改变后,再次替换数据 127 fn(node,newVal); 128 }) 129 }, 130 on(node,vm,expr,eventName){ 131 node.addEventListener(eventName,e=>{ 132 //调用call将vm实例(this)传到方法中去 133 vm[expr].call(vm,e); 134 }) 135 }, 136 updater:{ 137 modelUpdater(node,value){ 138 node.value=value 139 }, 140 htmlUpdater(node,value){ 141 node.innerHTML=value; 142 }, 143 textUpdater(node,value){ 144 145 node.textContent=value; 146 } 147 } 148 }
4、发布订阅
发布订阅主要靠的是数组关系,订阅就是放入函数(就是将订阅者添加到订阅队列中),发布就是让数组里的函数执行(在数据发生改变的时候,通知订阅者执行相应的操作)。消息的发布和订阅是在观察者的数据绑定中进行数据的——在get钩子函数被调用时进行数据的订阅(在数据编译时通过 new Watcher() 来对数据进行订阅
),在set钩子函数被调用时进行数据的发布。
1 //消息管理者(发布者),在数据发生变化时,通知订阅者执行相应的操作 2 class Dep{ 3 constructor(){ 4 this.subs=[]; 5 }; 6 //订阅 7 addSub(watcher){ 8 this.subs.push(watcher); 9 }; 10 //发布 11 notify(){ 12 this.subs.forEach(watcher=>watcher.update()); 13 } 14 } 15 //订阅者,主要是观察数据的变化 16 class Watcher{ 17 constructor(vm,expr,cb){ 18 this.vm=vm; 19 this.expr=expr; 20 this.cb=cb; 21 this.oldValue=this.get(); 22 }; 23 get(){ 24 Dep.target=this; 25 const value=CompileUtil.getVal(this.vm,this.expr); 26 Dep.target=null; 27 return value; 28 }; 29 update(){ 30 const newVal=CompileUtil.getVal(this.vm,this.expr); 31 if(this.oldValue!==newVal){ 32 this.cb(newVal); 33 } 34 } 35 } 36 //观察者 37 class Observer{ 38 constructor(data){ 39 this.observe(data); 40 }; 41 //使数据可响应 42 observe(data){ 43 if(data&&typeof data==="object"){ 44 this.defineReactive(data) 45 } 46 }; 47 defineReactive(data){ 48 Object.keys(data).forEach(key=>{ 49 const dep=new Dep(); 50 let val=data[key]; 51 this.observe(val);//深层次的监听 52 Object.defineProperty(data,key,{ 53 get(){ 54 //添加订阅者watcher(为每一个数据属性添加订阅者,以便实时监听数据属性的变化——订阅) 55 Dep.target&&dep.addSub(Dep.target); 56 //返回初始值 57 return val; 58 },set(newVal){ 59 if(val!==newVal){ 60 val=newVal; 61 //通知订阅者,数据变化了(发布) 62 dep.notify(); 63 return newVal; 64 } 65 } 66 }) 67 }) 68 } 69 } 70