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

MVVM在vue中的原理及其实现

时间:2023-03-31 15:39:09 vue.js

MVVM原理及其在vue中的实现1.理解MVVMMVVM——模型视图ViewModel:数据、视图、视图模型。三者与Vue的对应关系:view对应template,vm对应newVue({...}),model对应data。三者的关系:视图可以通过事件绑定影响模型,模型可以通过数据绑定影响视图,viewModel是连接模型和视图的连接器。MVVM框架的三大要素:响应性:Vue如何监听数据的每一次属性变化。数据劫持:使用Object.defineProperty(obj,'property',{})定义属性,将对象属性值的设置和访问(get,set)变成函数,分别在设置和获取对象的属性时对象调用执行。模板引擎:Vue模板是如何解析的,指令是如何处理的渲染:Vue模板是如何渲染成html的,渲染过程是怎样的2.实现方法实现compile来编译模板,包括编译元素(指令)和编译文本等,到达到初始化view的目的,还需要绑定update函数;实施Observe,监控所有数据,并对变化的数据发出通知;实现watcher,作为一个hub,接收observe的通知,执行compile中对应的update方法。结合以上方法,对外暴露mvvm方法。首先编辑一个html文件,如下:

{{obj.name}}
{{message}}{{obj.name}}
1.创建classMVVMclassMVVM{constructor(options){this.$el=选项.el;this.$data=options.data;if(this.$el){//数据劫持添加对象的所有属性getsetmethodnewObserver(this.$data);this.proxyData(this.$data);//用数据和元素编译newCompile(this.$el,this);}}//将对象的所有属性绑定到实例上,this.xxproxyData(data){//defineProperty分析:https://www.jianshu.com/p/8fe1382ba135Object.keys(data).forEach(key=>{Object.defineProperty(th是,键,{get(){返回数据[键];},set(newValue){数据[key]=newValue;}})})}}2.实现编译(compiletemplate)1.将真实的DOM移动到内存中的fragment中,因为fragment在内存中,所以运行速度更快2.编译:提取需要的元素节点v-model和文本节点{{}}3.将编译后的片段添加到DOM类中Compile{constructor(el,vm){this.el=this.isElementNode(el)?el:文档.querySelector(el);这个.vm=虚拟机;if(this.el){//1.将真正的DOM移动到内存Fragment中,因为fragment在内存中,所以操作更快letfragment=this.node2fragment(this.el);//2.编译=>提取所需的元素节点v-model和文本节点{{}}this.compile(fragment);//3.将编译后的片段添加到DOMthis.el.appendChild(fragment);}}//是否是元素节点isElementNode(el){//nodeType:1元素代表元素节点returnel.nodeType==1;}node2fragment(el){//在内存中,创建一个新的文档片段,让片段nt=document.createDocumentFragment();让第一个孩子;while(firstChild=el.firstChild){fragment.appendChild(firstChild);}//返回一个虚拟节点对象,其中包含所有的属性和方法返回片段;}//compilecompile(fragment){letchildNodes=fragment.childNodes;Array.from(childNodes).forEach(node=>{if(this.isElementNode(node)){//元素节点,编译元素this.compileElement(node);//如果有子节点,执行this.compile(node);}else{//文本节点,编译文本this.compileText(node);}})}//编译文本{{msg}}compileText(node){letexpr=node.textContent;//{{msg}}让reg=/\{\{([^}]+)\}\}/g;if(reg.test(expr)){CompileUtil.text(node,this.vm,expr);}}//编译元素v-compileElement(node){letattrs=node.attributes;Array.from(attrs).forEach(attr=>{//是否是指令if(attr.name.includes('v-')){letexpr=attr.value;//消息lettype=attr.name.split('-')[1];//模型CompileUtil[类型](node,this.vm,expr);}})}}CompileUtil={//文本处理text(node,vm,expr){letupdaterFn=this.updater.textUpdater;letvalue=this.getTextVal(vm,expr);//监听数据变化eg:因为{{message}}{{obj.name}},所以需要循环expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{newWatcher(vm,arguments[1],newValue=>{updaterFn&&updaterFn(node,this.getTextVal(vm,expr));})})updaterFn&&updaterFn(node,value);},//获取文本的keyeg:messagegetTextVal(vm,expr){returnexpr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{returnthis.getValue(vm,arguments[1]);//message})},//获取数据中对象的值getValue(vm,expr){letarr=expr.split('.');//处理对象obj.name=>[obj,name]returnarr.reduce((prev,next)=>{returnprev[next]},vm.$data);},//设置数据中对象的值setValue(vm,expr,value){letarr=expr.split('.');//处理对象obj.name=>[obj,name]arr.reduce((prev,next,curIndex)=>{if(curIndex==arr.length-1){returnprev[next]=value;}返回上一个[下一个];},vm.$data);},//输入框处理model(node,vm,expr){letupdaterFn=this.updater.modalUpdater;让值=this.getValue(vm,expr);node.addEventListener('input',(event)=>{this.setValue(vm,expr,event.target.value);})//监听数据变化newWatcher(vm,expr,newValue=>{updaterFn&&updaterFn(node,this.getValue(vm,expr));})updaterFn&&updaterFn(node,value);},updater:{textUpdater(node,value){node.textContent=value;},modalUpdater(node,value){node.value=value;}}}3.实现observe(数据监控/劫持)。vue采用的observe+sub/pub实现数据劫持。通过js原生方法Object.defineProperty()劫持各个属性的setter和getter,更改属性对应的数据。当向订阅者发布消息,然后触发相应的监听回调时,为什么要监听get而不是直接监听set呢?因为数据中有很多属性,有的会用到,有的可能不会用到。只有用过的才会去拿。我们不必去获取属性。在设置的时候,我们不需要关心避免不必要的重新渲染主要内容:递归遍历observe的数据对象,包括子属性对象的属性,添加setter和getter。类观察者{构造函数(数据){this.observe(数据);}//获取数据的键值observe(data){if(!data||typeofdata!='object')return;Object.keys(data).forEach(key=>{this.defineReactive(data,key,data[key]);this.observe(data[key]);})}//定义响应式defineReactive(obj,key,价值){让那个=这个;//每一个变化的数据都会对应一个数组,里面存放着所有更新的操作letdep=newDep();Object.defineProperty(obj,key,{enumerable:true,configurable:true,get(){console.log('observe',key,Dep.target);Dep.target&&dep.addSubs(Dep.target);返回value;},set(newValue){if(value!=newValue){//如果对象继续劫持that.observe(newValue);value=newValue;//通知更新部门通知();}}})}}实现数据劫持后,如何通知订阅者下一个任务?我们需要在监听数据的时候实现一个消息订阅者。具体方法是:为Store订阅者定义一个数组,数据变化通知(notify)订阅者,然后调用订阅者的update方法添加Dep类:classDep{constructor(){this.subs=[];}//添加订阅addSubs(watcher){this.subs.push(观察者);}//通知更新notify(){this.subs.forEach(watcher=>{watcher.update();})}}4.实现watcher(订阅中心)Observer与Compile之间的通信桥梁是Watcher订阅中心,其主要职责是:1、在实例化自身时将自己添加到属性订阅者(Dep)中,并与Observer建立连接;2.它必须有一个update()方法来建立与Compile的连接;3.当属性发生变化时,Observer中的dep.notify()会通知,然后调用自身(Watcher)的update()方法,触发Compile中绑定的回调实现更新。//观察者的作用是给需要改变的元素添加一个观察者,当数据改变时执行相应的方法classWatcher{constructor(vm,expr,cb){this.vm=vm;这个.expr=expr;这个.cb=cb;//获取旧值this.value=this.get();}//获取数据中对象的值getValue(vm,expr){letarr=expr.split('.');returnarr.reduce((prev,next)=>{returnprev[next]},vm.$data);}get(){Dep.target=this;letvalue=this.getValue(this.vm,this.expr);Dep.target=null;返回值;}//更新,外部调用的方法update(){letnewValue=this.getValue(this.vm,this.expr);如果(newValue!=this.value){this.cb(newValue);}}}