用Object.defineProperty实现本身的Vue和MVVM

什么是MVVM

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

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

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

MVVM

怎么实现MVVM

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

Object.defineProperty

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

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

可是从有了Object.defineProperty后,能够经过如下的方式为对象添加属性:node

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:bash

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,去掉以后:app

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.$ellet 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
相关文章
相关标签/搜索