用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
相關文章
相關標籤/搜索