用Object.defineProperty实现自己的Vue和MVVM

xiaoxiao2025-05-26  35

什么是MVVM

MVVM是Model-View-ViewModel的简写,即模型-视图-视图模型。Model指的是后端传递的数据。View指的是所看到的页面。ViewModel是mvvm模式的核心,它是连接view和model的桥梁。它有两个方向:

将Model转化成View,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。将View转化成Model,即将所看到的页面转化成后端的数据。实现的方式是:DOM事件事件监听。这两个方向都实现的,我们称之为数据的双向绑定。

总结:在MVVM的框架下View和Model是不能直接通信的。它们通过ViewModel来通信,ViewModel通常要实现一个observer观察者,当数据发生变化,ViewModel能够监听到数据的这种变化,然后通知到对应的视图做自动更新,而当用户操作视图,ViewModel也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定。并且MVVM中的View 和 ViewModel可以互相通信。MVVM流程图如下:

 

 

 

怎么实现MVVM

脏值检查:angularangular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。数据劫持:使用Object.defineProperty()方法把这些vm.data属性全部转成setter、getter方法。

Object.defineProperty

从前声明一个对象,并为其赋值,使用的以下的方式:

var obj = {}; obj.name = 'hanson'; 复制代码

但是从有了Object.defineProperty后,可以通过以下的方式为对象添加属性:

var obj={}; Object.defineProperty(obj,'name',{ value:'hanson' }); console.log(obj);//{} 复制代码

此时发现打印的结果为一个空对象,这是因为此时的enumerable属性默认为false,即不可枚举,所以加上enumerable后:

var obj={}; Object.defineProperty(obj,'name',{ enumerable: true, value:'hanson' }); console.log(obj);//{ name: 'hanson' } obj.name = 'beauty'; console.log(obj)//{ name: 'hanson' } 复制代码

发现改变obj.name之后打印的还是{name:'hanson'},这是因为此时writable为false,即不可以修改,所以加上writable后:

var obj={}; Object.defineProperty(obj,'name',{ writable :true, enumerable: true, value:'hanson' }); console.log(obj);//{ name: 'hanson' } obj.name = 'beauty'; console.log(obj)//{ name: 'beauty' } delete obj.name; console.log(obj);//{ name: 'beauty' } 复制代码

发现改变obj.name之后打印的是{name:'beauty'},这是因为此时configurable为false,即不可以删除,所以加上configurable后:

var obj={}; Object.defineProperty(obj,'name',{ configurable:true, writable :true, enumerable: true, value:'hanson' }); console.log(obj);//{ name: 'hanson' } obj.name = 'beauty'; console.log(obj)//{ name: 'beauty' } delete obj.name; console.log(obj);//{} 复制代码

但是上面这样和普通的对象属性赋值没有区别,要想实现数据劫持必须使用set和get:

var obj={}; Object.defineProperty(obj,'name',{ configurable:true, writable :true, enumerable: true, value:'hanson', get(){ console.log('get') return 'hanson' }, set(newVal){ console.log('set'+ newVal) } }); console.log(obj);//{ name: 'hanson' } obj.name = 'beauty'; console.log(obj)//{ name: 'beauty' } delete obj.name; console.log(obj);//{} 复制代码

此时发现会报错:TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute,因为出现set和get就不能有value或者writable,去掉之后:

var obj={}; Object.defineProperty(obj,'name',{ configurable:true,//如果不涉及删除可以属性可以不加 enumerable: true, get(){ console.log('get') return 'hanson' }, set(newVal){ console.log('set'+ newVal) } }); console.log(obj);//{ name: 'hanson' } obj.name = 'beauty'; console.log(obj)//{ name: 'beauty' } delete obj.name; console.log(obj);//{} 复制代码

Vue中MVVM组成部分

Observe:利用Object.defineProperty数据劫持data,所以vue不能新增属性必须事先定义,model->vm.dataCompile:在文档碎片中操作dom节点,遍历正则匹配替换data属性,view->vm.$elDep&&Watcher:利用发布订阅模式链接view和model

 

 

 

Vue的构造函数

function myVue(options){//{el:'#app',data:{a:{a:3},b:5}} this.$options = options;//将options挂载在vm.$options上 this._data = this.$options.data;//使用_data,后面会将data属性挂载到vm上 observe(this.$options.data);//数据劫持 } var vm = new myVue({el:'#app',data:{a:{a:3},b:5}}); 复制代码

Observe数据劫持

function observe(data){ if(typeof data !== 'object'){//不是对象不进行数据劫持 return } return new Observe(data); } //将model->vm.data function Observe(data){ for(let key in data){//遍历所有属性进行劫持 let val = data[key]; observe(val);//深入递归数据劫持exp:data:{a:{a:3},b:5}} Object.defineProperty(data,key,{ enumerable: true, get(){ return val//此时的val已经进行了数据劫持,exp:{a:3} }, set(newVal){ if(newVal === val ){//值不变则返回 return } val = newVal; observe(newVal);//新赋的值也必须进行数据劫持 } } } } 复制代码

data属性挂载到vm上

function myVue(options){//{el:'#app',data:{a:{a:3},b:5}} let self = this; this.$options = options; this._data = this.$options.data; observe(this.$options.data); for(let key in this._data){//会将data属性挂载到vm上,vm.a = {a:3} Object.defineProperty(self,key,{ enumerable: true, get(){ return self._data[key]; }, set(newVal){ self._data[key] = newVal;//会自动调用data某个属性的set方法,所以挂载data属性到vm上必须在劫持后执行 } } } } var vm = new myVue({el:'#app',data:{a:{a:3},b:5}}); conole.log(vm.a);//3 vm.a = 4; console.log(vm.a);//4 复制代码

Compilem视图模板编译

function myVue(options){//{el:'#app',data:{a:{a:3},b:5}} let self = this; this.$options = options; this._data = this.$options.data; observe(this.$options.data); for(let key in this._data){ Object.defineProperty(self,key,{ enumerable: true, get(){ return self._data[key]; }, set(newVal){ self._data[key] = newVal; } } } new Compile(options.el,this);//模板编译 } //el—>vm.$el function Compile (el, vm) { vm.$el=document.querySelector(el);//将视图挂载到vm.$el上 let fragment = document.createDocumentFragment(); while(child = vm.$el.firstChild){ fragment.appendChild(child);//将所有的DOM移动到内存中操作,避免版不必要DOM的渲染 } function repalce(fragment){ Array.form(fragmrnt.childNodes).forEach(node=>{//将类数组转化为数组,然后遍历每一个节点 let text=node.textContent,reg=/\{\{(.*)\}\}/;//获取节点的文本内容,并检测其中是否存在,exp:{{a.a}} if(nodeType===3&&//reg.test(text)){ let arr=RegExp.$1.split('.'),val=vm;//分割RegExp.$1为a.a => [a,a] arr.forEach(key=>val=val[key];);//vm => vm.a => vm.a.a=3 node.textContent=text.replace(reg,val);//替换{{a.a}} => 3 } if(node.childNodes){//递归遍历所有的节点 replace(node) } }) } replace(fragment);//模板替换,将{{xxxx}}替换成数据或者其他操作 vm.$el.appendChild(fragment); } 复制代码

Dep&&Watcher发布订阅

//发布者 function Dep () { this.subs=[]; } Dep.prototype.addSub=function (sub) {//添加订阅者 this.subs.push(sub) }; Dep.prototype.notify=function () {//通知订阅者 this.subs.forEach((sub)=>sub.update()) }; //订阅者 function Watcher (vm,exp,fn) { this.fn=fn; } Watcher.prototype.update=function () {//订阅者更新 this.fn(); }; 复制代码

Dep&&Watcher链接view和model

//el—>vm.$el function Compile (el, vm) { vm.$el=document.querySelector(el); let fragment = document.createDocumentFragment(); while(child = vm.$el.firstChild){ fragment.appendChild(child); } function repalce(fragment){ Array.form(fragmrnt.childNodes).forEach(node=>{ let text=node.textContent,reg=/\{\{(.*)\}\}/; if(nodeType===3&&//reg.test(text)){ let arr=RegExp.$1.split('.'),val=vm; arr.forEach(key=>(val=val[key]);); node.textContent=text.replace(reg,val); //创建一个订阅者用于更新视图 new Watcher(vm,RegExp.$1,function (newVal) { node.textContent = text.replace(reg,newVal); }); } if(node.childNodes){ replace(node) } }) } replace(fragment);//模板替换,将{{xxxx}}替换成数据或者其他操作 vm.$el.appendChild(fragment); } //Dep&&Watcher function Dep () { this.subs=[]; } Dep.prototype.addSub=function (sub) { this.subs.push(sub) }; Dep.prototype.notify=function () { this.subs.forEach((sub)=>sub.update()) }; function Watcher (vm,exp,fn) {//更新视图需要通过exp去获取数据,a.a this.fn=fn; this.vm=vm; this.exp=exp; Dep.target=this; var arr=exp.split('.'),val=vm; arr.forEach(key=>(val=val[key]);); Dep.target=null; } Watcher.prototype.update=function () { var arr=this.exp.split('.'),val=this.vm; arr.forEach(key=>(val=val[key]););//获取到更新后的值 this.fn(val);//更新视图 }; 复制代码 //将model->vm.data function Observe(data){ let dep = new Dep;//创建一个发布者,来存储所有的订阅者 for(let key in data){ let val = data[key]; observe(val); Object.defineProperty(data,key,{ enumerable: true, get(){ //添加订阅者,执行Observe的时候下面这行不执行,因为只用new Watcher时调用get时才会执行这行代码 Dep.target&&dep.addSub(Dep.target); return val }, set(newVal){ if(newVal === val ){ return } val = newVal; observe(newVal); dep.notify();//触发值的更新 } } } } //Dep&&Watcher function Dep () { this.subs=[]; } Dep.prototype.addSub=function (sub) { this.subs.push(sub) }; Dep.prototype.notify=function () { this.subs.forEach((sub)=>sub.update()) }; function Watcher (vm,exp,fn) { this.fn=fn; this.vm=vm; this.exp=exp; Dep.target=this; var arr=exp.split('.'),val=vm; arr.forEach(key=>(val=val[key]););//这里会调用vm.a的get和vm.a.a的get Dep.target=null; } Watcher.prototype.update=function () { var arr=this.exp.split('.'),val=this.vm; arr.forEach(key=>(val=val[key]););//这里会调用vm.a.a的get和vm.a.a的get,但是Dep.target=null,不会再添加重复添加这个订阅者 this.fn(val); }; 复制代码

实现双向数据绑定

function repalce(fragment){ Array.form(fragmrnt.childNodes).forEach(node=>{ let text=node.textContent,reg=/\{\{(.*)\}\}/; if(nodeType===3&&//reg.test(text)){ let arr=RegExp.$1.split('.'),val=vm; arr.forEach(key=>(val=val[key]);); node.textContent=text.replace(reg,val); new Watcher(vm,RegExp.$1,function (newVal) { node.textContent = text.replace(reg,newVal); }); } if(node.nodeType===1){//双向绑定一般为input,所以增加对DOM节点的处理 var attrs=node.attributes; Array.from(attrs).forEach(function (attr) {//{name:'v-model',value:'a.a'} var name=attr.name,exp=attr.value;//类似a.a if(name.indexOf('v-')==0){//判断是否有v-model node.value=vm[exp];//初次渲染DOM node.addEventListener('input',function (e) {//监听input改变vm的值 var newVal=e.target.value; vm[exp]=newVal }); new Watcher(vm,exp,function (newVal) {//监听vm值更改view刷新 node.value=newVal; }); } }) } if(node.childNodes){ replace(node) } }) } 复制代码

实现computed

//computed将computed挂载在vm.computed属性上 function myVue(options){//{el:'#app',data:{a:{a:3},b:5}} let self = this; this.$options = options; this._data = this.$options.data; observe(this.$options.data); for(let key in this._data){ Object.defineProperty(self,key,{ enumerable: true, get(){ return self._data[key]; }, set(newVal){ self._data[key] = newVal; } } } initComputed.call(this); new Compile(options.el,this); } function initComputed() {//computer:{c(){return this.a.a + this.b}} var vm=this,computed=this.$options.computed; Object.keys(computed).forEach(function (key) { Object.defineProperty(vm,key,{ enumerable: true, get:typeof computed[key]==='function'?computed[key]:computed[key].get }) }) } 复制代码

结语:

希望这篇文章能够让各位看官对Vue更熟悉,使用起来更顺手,如果以上有任何错误之处,希望提出并请指正,如果对Vue使用还不清楚的朋友,请参考Vue官网教程,本文参考:

什么是MVVM,MVC和MVVM的区别,MVVM框架VUE实现原理javascript设计模式之MVVM模式javascript设计模式之Observe模式Object.defineProperty API

作者:梦想攻城狮 链接:https://juejin.im/post/5b99215d5188255c520cfe22 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

转载请注明原文地址: https://www.6miu.com/read-5030730.html

最新回复(0)