VUE的數據雙向綁定

寫在前面的東西

Vue.js自從在github上開源以來就受到各方的極大關注,並在短暫的時間裏當即火了起來,如今已成爲最流行的前端框架之一;我也使用vue有一段時間了,對vue的雙向綁定有必定的理解,在這和你們分享個人愚見,有錯誤的地方望你們給予指正。html

一、概述

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

圖片描述

原理圖告訴咱們,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 的方法)vue

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這個方法,要了解更多能夠點擊這裏node

3.實現observer

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

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。github

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方法通知到訂閱者;咱們如今合併一下這兩段代碼segmentfault

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中,反過來亦然。app

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的功能,並且還很殘缺,好比對象的深層綁定,以及計算屬性都尚未加入,做爲後續部分吧,最後得膜拜一下尤神,太牛叉了!

參考資料:
1.https://segmentfault.com/a/11...
2.https://segmentfault.com/a/11...
3.https://github.com/youngwind/...

相關文章
相關標籤/搜索