1. 程式人生 > >VUE的數據雙向綁定

VUE的數據雙向綁定

proto ole jason new 影響 table split 一點 正式

1、概述

讓我們先來看一下官網的這張數據綁定的說明圖:

技術分享圖片

原理圖告訴我們,a對象下面的b屬性定義了getter、setter對屬性進行劫持,當屬性值改變是就會notify通知watch對象,而watch對象則會notify到view上對應的位置進行更新(這個地方還沒講清下面再講),然後我們就看到了視圖的更新了,反過來當在視圖(如input)輸入數據時,也會觸發訂閱者watch,更新最新的數據到data裏面(圖中的a.b),這樣model數據就能實時響應view上的數據變化了,這樣一個過程就是數據的雙向綁定了。

看到這裏就會第一個疑問:那麽setter、getter是怎樣實現的劫持的呢?答案就是vue運用了es5中Object.defineProperty()這個方法,所以要想理解雙向綁定就得先知道Object.defineProperty是怎麽一回事了;

2.Object.defineProperty

它是es5一個方法,可以直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 並返回這個對象,對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。數據描述符是一個擁有可寫或不可寫值的屬性。存取描述符是由一對 getter-setter 函數功能來描述的屬性。描述符必須是兩種形式之一;不能同時是兩者
屬性描述符包括:configurable(可配置性相當於屬性的總開關,只有為true時才能設置,而且不可逆)、Writable(是否可寫,為false時將不能夠修改屬性的值)、Enumerable(是否可枚舉,為false時for..in以及Object.keys()將不能枚舉出該屬性)、get(一個給屬性提供 getter 的方法)、set(一個給屬性提供 setter 的方法)

var o = {name:‘vue‘};
Object.defineProperty(o, "age",{ value : 3,
                               writable : true,//可以修改屬性a的值
                               enumerable : true,//能夠在for..in或者Object.keys()中枚舉
                               configurable : true//可以配置
                               });

Object.keys(o)//[‘name‘,‘age‘]
o.age = 4;
console.log(o.age) //4

var bValue;
Object.defineProperty(o, "b", {
                               get : function(){ 
                                         return bValue; 
                                     },
                               set : function(newValue){ 
                                       console.log(‘haha..‘)
                                       bValue = newValue; 
                                     },
                               enumerable : true,//默認值是false 及不能被枚舉
                               configurable : true//默認也是false
                               });
 o.b = ‘something‘;
//haha..

上面分別給出了對象屬性描述符的數據描述符和存取描述的例子,註意一點是這兩種不能同時擁有,也就是valuewritable不能和getset同時具備。在這裏只是很粗淺的說了一下Object.defineProperty這個方法,要了解更多可以點擊這裏

3.實現observer

我們在上面一部分講到了es5的Object.defineProperty()這個方法,vue正式通過它來實現對一個對象屬性的劫持的,在創建實例的時候vue會對option中的data對象進行一次數據格式化或者說初始化,給每個data的屬性都設置上get/set進行對象劫持,代碼如下:

function Observer(data){
    this.data = data;
    if(Array.isArray(data)){
        protoAugment(data,arrayMethods); //arrayMethods實現對Array.prototype原型方法的拷貝;
        this.observeArray(data);
    }else{
        this.walk(data);
    }
    
}

Observer.prototype = {
    walk:function walk(data){
        var _this = this;
        Object.keys(data).forEach(function(key){
            _this.convert(key,data[key]);
        })
    },
    convert:function convert(key,val){
        this.defineReactive(this.data,key,val);
    },
    defineReactive:function defineReactive(data,key,val){
        var ochildOb = observer(val);
        var _this = this;
        Object.defineProperty(data,key,{
            configurable:false,
            enumerable:true,
            get:function(){
                console.log(`i get the ${key}-->${val}`)
                return val;
            },
            set:function(newVal){
                if(newVal == val)return;
                console.log(`haha.. ${key} changed oldVal-->${val} newVal-->${newVal}`);
                val = newVal;
                observer(newVal);//在這裏對新設置的屬性再一次進行get/set     
            }            
        })
    },
    observeArray:function observeArray(items){
        for (var i = 0, l = items.length; i < l; i++) {
            observer(items[i]);
         }
    }
}
function observer(data){
    if(!data || typeof data !==‘object‘)return;
    return new Observer(data);
}
//讓我們來試一下
var obj = {name:‘jasonCloud‘};
var ob = observer(obj);
obj.name = ‘wu‘;
//haha.. name changed oldVal-->jasonCloud newVal-->wu
obj.name;
//i get the name-->wu

到這一步我們只實現了對屬性的set/get監聽,但並沒實現變化後notify,那該怎樣去實現呢?在VUE裏面使用了訂閱器Dep,讓其維持一個訂閱數組,但有訂閱者時就通知相應的訂閱者notify。

let _id = 0;
/*
  Dep構造器用於維持$watcher檢測隊列;
*/
function Dep(){
    this.id = _id++;
    this.subs = [];
}

Dep.prototype = {
    constructor:Dep,
    addSub:function(sub){
        this.subs.push(sub);
    },
    notify:function(){
        this.subs.forEach(function(sub){
            if(typeof sub.update == ‘function‘)
            sub.update();
        })
    },
    removeSub:function(sub){
        var index = this.subs.indexOf(sub);
        if(index >-1)
        this.subs.splice(index,1);
    },
    depend:function(){
        Dep.target.addDep(this);
    }
}

Dep.target = null; //定義Dep的一個屬性,當watcher時Dep.targert=watcher實例對象

在這裏構造器Dep,維持內部一個數組subs,當有訂閱時就addSub進去,通知訂閱者更新時就會調用notify方法通知到訂閱者;我們現在合並一下這兩段代碼

function Observer(data){
    //省略的代碼..
    this.dep = new Dep();
    //省略的代碼..
    
}

Observer.prototype = {

    //省略的代碼..
    
    defineReactive:function defineReactive(data,key,val){

        //省略的代碼..

        var dep = new Dep();

        Object.defineProperty(data,key,{
            configurable:false,
            enumerable:true,
            get:function(){
                if(Dep.target){
                    dep.depend();
                    //省略的代碼..
                }
                return val;
            },
            set:function(newVal){
                //省略的代碼..
                dep.notify();         
            }            
        })
    },
    observeArray:function observeArray(items){
        for (var i = 0, l = items.length; i < l; i++) {
            observer(items[i]);
         }
    }
}

function observer(data){
    if(!data || typeof data !==‘object‘)return;
    return new Observer(data);
}

上面代碼中有一個protoAugment方法,在vue中是實現對數組一些方法的重寫,但他並不是直接在Array.prototype.[xxx]直接進行重寫這樣會影響到所有的數組中的方法,顯然是不明智的,vue很巧妙的進行了處理,使其並不會影響到所有的Array上的方法,代碼可以點擊這裏

到這裏我們實現了數據的劫持,並定義了一個訂閱器來存放訂閱者,那麽誰是訂閱者呢?那就是Watcher,下面讓我們看看怎樣實現watcher

4.實現一個Watcher

watcher是實現view視圖指令及數據和model層數據聯系的管道,當在執行編譯時候,他會把對應的屬性創建一個Watcher對象讓他和數據層model建立起聯系。但數據發生變化是會觸發update方法更新到視圖上view中,反過來亦然。

function Watcher(vm,expOrFn,cb){
    this.vm = vm;
    this.cb = cb;
    this.expOrFn = expOrFn;
    this.depIds = {};
    var value = this.get(),valuetemp;
    if(typeof value === ‘object‘ && value !== null){
        if(Array.isArray(value)){
            valuetemp = [];
            for(var i = 0,len = value.length;i<len;i++){
                valuetemp.push(value[i]);
            }
        }else{
            valuetemp = {};
            for(var j in value){
                valuetemp[j] = value[j];
            }
        }
        this.value = valuetemp;
    }else{
        this.value = value;
    }
     
};
Watcher.prototype = {
    update:function(){
        this.run();
    },
    run:function(){
        var val = this.get(),valuetemp;
        var oldVal = this.value;
        if(val!==oldVal){
            if(typeof val === ‘object‘ && val !== null){
                if(Array.isArray(val)){
                    valuetemp = [];
                    for(var i = 0,len = val.length;i<len;i++){
                        valuetemp.push(val[i]);
                    }
                }else{
                    valuetemp = {};
                    for(var j in val){
                        valuetemp[j] = val[j];
                    }
                }
                this.value = valuetemp;
            }else{
                this.value = val;
            }
            this.cb.call(this,val,oldVal);
        }
    },
    get:function(){
        Dep.target = this;
        var val = this.getVMVal();
        Dep.target = null;
        return val;
    },
    getVMVal:function(){
        var exps = this.expOrFn.split(‘.‘);
        var val = this.vm._data;
        exps.forEach(function(key){
            val = val[key];
        })

        return val;
    },
    addDep:function(dep){
        if(!this.depIds.hasOwnProperty(dep.id)){
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    }

}

到現在還差一步就是將我們在容器中寫的指令和{{}}讓他和我們的model建立起連續並轉化成,我們平時熟悉的html文檔,這個過程也就是編譯;編譯簡單的實現就是將我們定義的容器裏面所有的子節點都獲取到,然後通過對應的規則進行轉換編譯,為了提高性能,先創建一個文檔碎片createDocumentFragment(),然後操作都在碎片中進行,等操作成功後一次性appendChild進去;

function Compile(el,vm){
    this.$vm = vm;
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if(this.$el){
        this.$fragment = this.nodeToFragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
        this.$vm.$option[‘mount‘] && this.$vm.$option[‘mount‘].call(this.$vm);
    }
}

5.實現一個簡易版的vue

到目前為止我們可以實現一個簡單的數據雙向綁定了,接下來要做的就是對這一套流程進行整合了,不多說上碼

function Wue(option){
    this.$option = option;
    var data = this._data = this.$option.data;
    var _this = this;

    //數據代理實現數據從vm.xx == vm.$data.xx;
    Object.keys(data).forEach(function(val){
        _this._proxy(val)
    });

    observer(data)
    this.$compile = new Compile(this.$option.el , this);
}

Wue.prototype = {
    $watch:function(expOrFn,cb){
        return new Watcher(this,expOrFn,cb);
    },
    _proxy:function(key){
        var _this = this;
        Object.defineProperty(_this,key,{
            configurable: false,
            enumerable: true,
            get:function(){
                return _this._data[key];
            },
            set:function(newVal){
                _this._data[key] = newVal;
            }
        })
    }
}

在這裏定義了一個Wue構造函數,當實例化的時候他會對option的data屬性進行格式化(劫持),然後再進行編譯,讓數據和視圖建立起聯系;在這裏用_proxy進行數據代理是為了當訪問數據時可以直接vm.xx而不需要vm._data.xx;

VUE的數據雙向綁定