Vue雙向綁定的實現原理系列(三):監聽器Observer和訂閱者Watcher

監聽器Observer和訂閱者Watcher

實現簡單版Vue的過程,主要實現{{}}、v-model和事件指令的功能

主要分爲三個部分

github源碼vue

1.數據監聽器Observer,可以對數據對象的全部屬性進行監聽;
    實現數據的雙向綁定,首先要對數據進行劫持監聽,因此咱們須要設置一個監聽器Observer,用來監聽全部屬性
 
 2.Watcher將數據監聽器和指令解析器鏈接起來,數據的屬性變更時,執行指令綁定的相應回調函數,
    1.若是屬性發上變化了,就須要告訴訂閱者Watcher看是否須要更新。
    
 3.指令解析器Compile,
    對每一個節點元素進行掃描和解析,將相關指令對應初始化成一個訂閱者Watcher

 由於訂閱者是有不少個,因此咱們須要有一個消息訂閱器Dep來專門收集這些訂閱者,而後在監聽器Observer和訂閱者Watcher之間進行統一管理的。=

監聽器Observergit

Observer是一個數據監聽器,核心是前面一直談論的Object.defineProperty(),
對全部屬性監聽,利用遞歸來遍歷全部的屬性值,對其進行Object.defineProperty()操做:
function definReactive(data,key,val){
        observers(val);//遞歸全部子屬性
    
        Object.defineProperty(data,key,{
            enumerable:true,
            configurable:true,
            get:function(){
                console.log('屬性'+key+'執行get');
                return val;
            },
            set:function(newVal){
                val = newVal;
                console.log('屬性:'+key+'以及被監聽,如今值爲:'+newVal.toString());
            }
        })
    }
    
    function observers(data){
        if(!data || typeof data!='object'){
            return;
        }
        Object.keys(data).forEach(function(key){
            definReactive(data,key,data[key]);
        })
    }
    var library = {
        book1:{name:''},
        book2:''
    }
    observers(library);

    library.book1.name = 'vue書籍';
    library.book2 = '沒有書';
    //屬性book1執行get
    //屬性:name以及被監聽,如今值爲:vue書籍
    //屬性:book2以及被監聽,如今值爲:沒有書

接下來建立一個收集全部訂閱者的訂閱器Dep,閱器Dep主要負責收集訂閱者,而後再屬性變化的時候執行對應訂閱者的更新函數,
再改寫一下訂閱器Observer,建立一個observer.js:github

function Observe(data){
        this.data = data;
        this.walk(data);
    }
    Observe.prototype = {
        walk:function(data){
            var self = this;
            Object.keys(data).forEach(function(key) {
                self.defineReactive(data, key, data[key]);
            });
        },
        defineReactive:function(data,key,val){
            observers(val);//遞歸全部子屬性
            var dep = new Dep();

            Object.defineProperty(data,key,{
                enumerable:true,
                configurable:true,
                get:function(){
                    if(是否須要添加訂閱者){
                        dep.addSub(Watcher);//在這裏添加一個訂閱者
                    }
                    console.log('屬性'+key+'執行get');
                    return val;
                },
                set:function(newVal){
                    if(val === newVal){
                        return;
                    }
                    val = newVal;
                    dep.notify();//若是數據變化,通知全部訂閱者
                    console.log('屬性:'+key+'以及被監聽,如今值爲:'+newVal.toString());
                }
            })
        }
    }
    function observers(data){
        if(!data || typeof data!='object'){
            return;
        }
        return new Observe(data);
    }
       

    /**Dep:建立一個能夠容納訂閱者的消息訂閱器
     * **/

    function Dep(){
        this.subs = [];
    }
    Dep.prototype = {
        addSub:function(sub){//添加訂閱者
            this.subs.push(sub);
        },
        notify:function(){//通知訂閱者
            this.subs.forEach(function(sub){
                sub.update();
            })
        }
    }
    
    能夠看出,訂閱器Dep,添加一個訂閱者是在Object.defineProperty()的get裏面,這是爲了讓Watcher初始化進行觸發,
    所以要判斷是否是須要添加訂閱者,後面解釋。在set裏面,若是數據變化,就會通知全部的訂閱者,訂閱者就會去執行對應的更新的函數
    以上,一個完整的訂閱器完成。

訂閱者Watchersegmentfault

Watcher在初始化的時候要將本身添加進訂閱者Dep中,如何作到:

已經知道監聽器Observer是在get函數執行了添加訂閱者Wather的操做的,設計模式

因此咱們只要在訂閱者Watcher初始化的時候觸發對應的get函數,去執行添加訂閱者操做便可,緩存

那要如何觸發get的函數:bash

只要獲取對應的屬性值就能夠觸發了,核心緣由就是由於咱們使用了Object.defineProperty()進行數據監聽。函數

注意:this

咱們只要在訂閱者Watcher初始化的時候才須要添加訂閱者,因此須要作一個判斷操做,prototype

所以能夠在訂閱器上作一下手腳:在Dep.target上緩存下訂閱者,添加成功後再將其去掉就能夠了。

建立一個watcher.js

function Watcher(vm,exp,cb){
        this.cb = cb;
        this.vm = vm;
        this.exp = exp;
        this.value = this.get();//將本身添加到訂閱器的操做
    }
    Watcher.prototype = {
        update:function () {
            this.run();
        },
        run:function () {
            var value = this.vm.data[this.exp];
            var oldVal = this.value;
            if(value != oldVal){
                this.value = value;
                this.cb.call(this.vm,value,oldVal);
            }
        },
        get:function () {
            Dep.target = this;//緩存本身
            var value = this.vm.data[this.exp];//強制執行監聽器observer裏的Object.defineProperty()裏的get函數
            Dep.target = null;//釋放本身
            return value;
        }
    }

再調整下observer.js的defineReactive函數裏的get操做:

defineReactive:function(data,key,val){
        observers(val);//遞歸全部子屬性
        var dep = new Dep();

        Object.defineProperty(data,key,{
            enumerable:true,
            configurable:true,
            get:function(){
                if(Dep.target){
                    dep.addSub(Dep.target);//在這裏添加一個訂閱者
                }
                console.log('屬性'+key+'執行get');
                return val;
            },
            set:function(newVal){
                if(val === newVal){
                    return;
                }
                val = newVal;
                dep.notify();//若是數據變化,通知全部訂閱者
                console.log('屬性:'+key+'以及被監聽,如今值爲:'+newVal.toString());
            }
        })
    }
    //Dep加個target屬性
    function Dep(){
        this.subs = [];
        this.target = null;
    }

簡單版的Watcher設計好了,
只要將Observer和Watcher關聯起來,就能夠實現一個簡單的雙向綁定數據了。
這裏沒有尚未設計解析器Compile,因此對於模板數據咱們都進行寫死處理:
模板有個節點,id爲name,雙向數據綁定的變量name,這裏大框號暫時沒有用:

<body> 
    <h1 id="name">{{name}}</h1> 
</body>
selVue.js 關聯Observer和Watcher
function SelfVue(data,el,exp){
        this.data = data;
        observers(data);
        el.innerHTML = this.data[exp];//初始化模板數據的值
        
        new Watcher(this,exp,function(value){
            el.innerHTML = value;
        });
        return this;
    }

頁面上實現雙向數據綁定:

<h1 id="name">{{name}}</h1>
    <script src="js/observer.js"></script>
    <script src="js/watcher.js"></script>
    <script src="js/selfVue.js"></script>
    <script>
        var ele = document.querySelector('#name');
        var self_Vue = new SelfVue({
            name:'第一次顯示數據'
        },ele,'name');
    
        window.setTimeout(function(){
            console.log('值變了');
            self_Vue.data.name = '從新賦值了';
        },2000);
    </script>

打開頁面,能夠看到頁面剛開始顯示了是「第一次顯示數據」,過了2s後就變成「從新賦值了」了。到這裏,完成了一部分

注意:賦值的時候是 self_Vue.data.name = '從新賦值了',可是但願是 self_Vue.name = '從新賦值了',
須要在new SelVue的時候作個代理,讓訪問self_Vue的屬性代理爲訪問self_Vue.data的屬性,
實現原理仍是使用Object.defineProperty()對屬性再包一層,

修改selVue.js:

function SelfVue(data,el,exp){
        var self = this;
        this.data = data;
    
        Object.keys(data).forEach(function (key) {
            self.proxyKeys(key);//綁定代理屬性
        });
        
        observers(data);
        el.innerHTML = this.data[exp];//初始化模板數據的值
        new Watcher(this,exp,function(value){
            el.innerHTML = value;
        });
        return this;
    }
    
    SelfVue.prototype = {
        proxyKeys:function(key){
            var self = this;
            Object.defineProperty(this,key,{
                enumerable:false,
                configurable:true,
                get:function proxyGetter(){
                    return self.data[key];
                },
                set:function proxySetter(newVal){
                    self.data[key] = newVal;
                }
            })
        }
    }

    //這下咱們就能夠直接經過self_Vue.name = '從新賦值了'的形式來進行改變模板數據。

至此監聽器Observer和訂閱者Watcher功能基本完成,後面再加上指令解析器compile的功能!

系列文章的目錄:

Vue雙向綁定的實現原理系列(一):Object.defineproperty
Vue雙向綁定的實現原理系列(二):設計模式
Vue雙向綁定的實現原理系列(三):監聽器Observer和訂閱者Watcher
Vue雙向綁定的實現原理系列(四):補充指令解析器compile

相關文章
相關標籤/搜索