用Object.defineProperty实现自己的Vue和MVVM
什么是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.data
- Compile:在文档碎片中操作dom节点,遍历正则匹配替换data属性,view->vm.$el
- Dep&&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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。