当前位置: 首页 > Web前端 > vue.js

MVVM模式下双向数据绑定分析

时间:2023-03-31 17:03:41 vue.js

什么是MVVM模式MVVM是由MVC发展而来的。在传统的MVC模式中,Model是数据层,View层只负责展示数据,Controller层负责数据分析。但是对于复杂的数据结构,继续沿用MVC的设计思路,将数据解析部分放在Controller中,那么Controller就会变得相当臃肿(Controller并不是为了处理数据解析而设计的),所以开发者专门为数据分析:ViewModel,也就是MVVM模式。当用户操作View(视图)时,ViewModal感知到变化,然后通知Modal相应的变化;反之,当Modal(数据)发生变化时,ViewModal也能感知到变化,并相应地更新View。MVVM模式如何实现mvvm的实现主要包括两个方面,数据变化更新视图,视图变化更新数据:关键是数据如何更新视图,因为视图更新数据实际上可以通过事件监听,比如在input标签中监听'input'事件就可以实现。那么我们就着重分析当数据发生变化时,如何更新视图。实现数据双向绑定的方式有很多种:比较有名的一种是vue的数据劫持方式;vue3版本之前,采用数据劫持结合发布订阅者模式实现数据的双向绑定;设计模式——发布-订阅发布-订阅模式定义了一对多的依赖关系,允许多个订阅者对象同时监听一个主题对象。当主题对象改变其状态时,它会通知所有订阅者对象,以便它们可以自动更新它们的状态。数据劫持所谓数据劫持,是指在访问或修改对象的某个属性、执行附加操作或修改返回结果时,通过一段代码拦截这种行为。典型的是Object.defineProperty()和ES2015中新的Proxy对象。Object.defineProperty()可以控制对象属性的一些特有操作,比如读写权限,是否可以枚举。这里主要研究其对应的两个描述属性get和setvaro={};varbValue;Object.defineProperty(o,"b",{get:function(){console.log('get')returnbValue;},set:function(newValue){console.log('set')bValue=newValue;},enumerable:true,configurable:true});o.b=38;//triggersetconsole.log(o.b)ofobjecto//triggergetapireferenceofobjecto:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty这两个基于数据劫持的方式绑定mvvm必须实现以下几点:数据监听器(观察者)监听数据中的所有数据,通过Object.defineProperty,调用getter和setter方法劫持数据,数据调用就是触发get方法,数据变化时触发set方法;/**监听数据变化**/observer(data){Object.keys(data).forEach(key=>{letvalue=data[key];letdep=newDep();Object.defineProperty(data,key,{configurable:true,enumerable:true,get(){//数据调用触发get,once调用数据将添加到订阅中心if(Dep.target){dep.addSub(Dep.target);}返回值;},set(newValue){console.log("set",newValue);如果(新值!==值)值=新值;//一旦数据改变,通知订阅者dep.notify(newValue);}})})}指示解析器(编译)获取调用相应数据的节点(文本节点或标签节点)并替换最新的数据,如{{}};循环遍历页面所有节点,获取所有文本节点或标签,判断是文本节点还是标签,获取节点数据,创建订阅对象;compile(el){//获取需要挂载的根节点letelement=document.querySelector(el);this.compileNode(element);}compileNode(element){//模板解析letchildNodes=element.childNodes;//console.log(childNodes);Array.from(childNodes).forEach(node=>{//如果是文本节点if(node.nodeType==3){//text//console.log(节点);让nodeContent=node.textContent;//console.log(nodeContent);让reg=/\{\{\s*(\S*)\s*\}\}/;if(reg.test(nodeContent)){//console.log("("+RegExp.$1+")");node.textContent=this._data[RegExp.$1];//创建一个订阅者newWatcher(this,RegExp.$1,newValue=>{node.textContent=newValue;});}}elseif(node.nodeType==1){//如果是标签letattrs=node.attributes;//控制台日志(属性);Array.from(attrs).forEach(attr=>{//console.log(attr);letattrName=attr.name;letattrValue=attr.value;//console.log(attrName);if(attrName.indexOf("k-")==0){attrName=attrName.substr(2);缺点ole.log(属性名);if(attrName=="model"){node.value=this._data[attrValue];}node.addEventListener("input",e=>{//console.log(e.target.value);this._data[attrValue]=e.target.value;})//创建订阅者newWatcher(this,attrValue,newValue=>{node.value=newValue;});}})}if(node.childNodes.length>0){this.compileNode(node);}})}数据订阅中心(Dep)功能是添加订阅者和通知订阅者,具有存储和分发功能,发布者和订阅者都需要依赖订阅中心,任何调用数据都会添加到订阅中间,并通知相应的订阅者;//订阅中心,功能是添加订阅者并通知订阅者classDep{constructor(){this.subs=[];}addSub(sub){this.subs.push(sub);}notify(newValue){this.subs.forEach(v=>{v.update(newValUE);})}}当初始化订阅者(Watcher)时,所有被调用的节点都会被创建为订阅者,当数据发生变化时,会触发相应的update更新回调函数;//初始化new并生成n个watcher对象,并传入相应的回调//SubscriberclassWatcher{constructor(vm,exp,cb){//自己缓存避免重复调用Dep.target=this;vm._data[exp];这个.cb=cb;Dep.target=null}update(newValue){console.log("更新",newValue);这个.cb(新值);}}整合Observer、Compile和Watcher,使用Observer监测自己的模型数据变化,使用Compile解析编译模板指令,最后使用Watcher搭建Observer和Compile之间的沟通桥梁,实现数据变化->视图更新;viewinteractionchange(input)->datamodelchangetwo-waybindingeffectProxy数据代理ES2015规范中正式加入了Proxy。在数据劫持问题上,Proxy可以看作是Object.defineProperty()的升级版。外部对对象的访问必须经过这一层拦截。所以它可以劫持整个对象并返回一个新的对象而不是对象的一个??属性,所以不需要遍历键。但是仍然不支持对象嵌套,支持数组push、pop、shiftproxy构造函数:varproxy=newProxy(target,handler);有两个参数:target是Proxy包裹的代理对象(可以是任何类型,包括原始数组、函数,甚至是另一个代理)。处理程序是声明代理目标的某些操作的对象,其属性是定义执行操作时代理行为的函数。vararr=[1,2,3]varhandle={//target目标对象键属性名receiver实际接受对象get(target,key,receiver){console.log(`get${key}`)//反射相当于映射到目标对象returnReflect.get(target,key,receiver)},set(target,key,value,receiver){console.log(`set${key}`)returnReflect.set(target,key,value,receiver)}}//arr要拦截的对象,handle定义拦截行为varproxy=newProxy(arr,handle)proxy.push(4)但是新标准也有缺点,就是:Proxy兼容性不如Object.defineProperty()(caniuse资料显示QQ浏览器和百度浏览器不支持Proxy,这对于国内移动开发来说大概是不能接受的,但是都支持Object.defineProperty())无法使用Polyfill来处理兼容性总结数据绑定只是MVVM模型中的冰山一角。比如在代码实现过程中,订阅者直接更新数据修改DOM。是否可以结合DOM操作的高性能消耗来提高效率?引出了我们经常听到的一系列Virtual-DOM(虚拟DOM树)、diff操作等。如果你对三大框架的底层原理感兴趣,可以继续探索。