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

什么是MVVM

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

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

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

 

MVVM

 

 

怎么实现MVVM

  1. 脏值检查:angularangular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。
  2. 数据劫持:使用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组成部分

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

 

图解Vue的MVVM

 

 

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官网教程,本文参考:

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


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值