­

Vue中的三種Watcher

Vue中的三種Watcher

Vue可以說存在三種watcher,第一種是在定義data函數時定義數據的render watcher;第二種是computed watcher,是computed函數在自身內部維護的一個watcher,配合其內部的屬性dirty開關來決定computed的值是需要重新計算還是直接復用之前的值;第三種就是whtcher api了,就是用戶自定義的export導出對象的watch屬性;當然實際上他們都是通過class Watcher類來實現的。

描述

Vue.js的數據響應式,通常有以下的的場景:

  • 數據變->使用數據的視圖變。
  • 數據變->使用數據的計算屬性變->使用計算屬性的視圖變。
  • 數據變->開發者主動註冊的watch回調函數執行。

三個場景,對應三種watcher

  • 負責視圖更新的render watcher
  • 執行計算屬性更新的computed watcher
  • 用戶註冊的普通watcher api

render watcher

render watcher中,響應式就意味着,當數據中的值改變時,在視圖上的渲染內容也需要跟着改變,在這裡就需要一個視圖渲染與屬性值之間的聯繫,Vue中的響應式,簡單點來說分為以下三個部分:

  • Observer: 這裡的主要工作是遞歸地監聽對象上的所有屬性,在屬性值改變的時候,觸發相應的Watcher
  • Watcher: 觀察者,當監聽的數據值修改時,執行響應的回調函數,在Vue裏面的更新模板內容。
  • Dep: 鏈接ObserverWatcher的橋樑,每一個Observer對應一個Dep,它內部維護一個數組,保存與該Observer相關的Watcher

根據上面的三部分實現一個功能非常簡單的Demo,實際Vue中的數據在頁面的更新是異步的,且存在大量優化,實際非常複雜。
首先實現Dep方法,這是鏈接ObserverWatcher的橋樑,簡單來說,就是一個監聽者模式的事件總線,負責接收watcher並保存。其中subscribers數組用以保存將要觸發的事件,addSub方法用以添加事件,notify方法用以觸發事件。

function __dep(){
    this.subscribers = [];
    this.addSub = function(watcher){
        if(__dep.target && !this.subscribers.includes(__dep.target) ) this.subscribers.push(watcher);
    }
    this.notifyAll = function(){
        this.subscribers.forEach( watcher => watcher.update());
    }
}

Observer方法就是將數據進行劫持,使用Object.defineProperty對屬性進行重定義,注意一個屬性描述符只能是數據描述符和存取描述符這兩者其中之一,不能同時是兩者,所以在這個小Demo中使用gettersetter操作的的是定義的value局部變量,主要是利用了let的塊級作用域定義value局部變量並利用閉包的原理實現了gettersetter操作value,對於每個數據綁定時都有一個自己的dep實例,利用這個總線來保存關於這個屬性的Watcher,並在set更新數據的時候觸發。

function __observe(obj){
    for(let item in obj){
        let dep = new __dep();
        let value = obj[item];
        if (Object.prototype.toString.call(value) === "[object Object]") __observe(value);
        Object.defineProperty(obj, item, {
            configurable: true,
            enumerable: true,
            get: function reactiveGetter() {
                if(__dep.target) dep.addSub(__dep.target);
                return value;
            },
            set: function reactiveSetter(newVal) {
                if (value === newVal) return value;
                value = newVal;
                dep.notifyAll();
            }
        });
    }
    return obj;
}

Watcher方法傳入一個回調函數,用以執行數據變更後的操作,一般是用來進行模板的渲染,update方法就是在數據變更後執行的方法,activeRun是首次進行綁定時執行的操作,關於這個操作中的__dep.target,他的主要目的是將執行回調函數相關的數據進行sub,例如在回調函數中用到了msg,那麼在執行這個activeRun的時候__dep.target就會指向this,然後執行fn()的時候會取得msg,此時就會觸發msgget(),而get中會判斷這個__dep.target是不是空,此時這個__dep.target不為空,上文提到了每個屬性都會有一個自己的dep實例,此時這個__dep.target便加入自身實例的subscribers,在執行完之後,便將__dep.target設置為null,重複這個過程將所有的相關屬性與watcher進行了綁定,在相關屬性進行set時,就會觸發各個watcherupdate然後執行渲染等操作。

function __watcher(fn){
    this.update = function(){
        fn();
    }
    
    this.activeRun = function(){
        __dep.target = this;
        fn();
        __dep.target = null;
    }
    this.activeRun();
}

這是上述的小Demo的代碼示例,其中上文沒有提到的__proxy函數主要是為了將vm.$data中的屬性直接代理到vm對象上,兩個watcher中第一個是為了打印並查看數據,第二個是之前做的一個非常簡單的模板引擎的渲染,為了演示數據變更使得頁面數據重新渲染,在這個Demo下打開控制台,輸入vm.msg = 11;即可觸發頁面的數據更改,也可以通過在40行添加一行console.log(dep);來查看每個屬性的dep綁定的watcher

<!DOCTYPE html>
<html>
<head>
    <title>數據綁定</title>
</head>
<body>
    <div id="app">
        <div>{{msg}}</div>
        <div>{{date}}</div>
    </div> 
</body>
<script type="text/javascript">

    var Mvvm = function(config) {
        this.$el = config.el;
        this.__root = document.querySelector(this.$el);
        this.__originHTML = this.__root.innerHTML;

        function __dep(){
            this.subscribers = [];
            this.addSub = function(watcher){
                if(__dep.target && !this.subscribers.includes(__dep.target) ) this.subscribers.push(watcher);
            }
            this.notifyAll = function(){
                this.subscribers.forEach( watcher => watcher.update());
            }
        }


        function __observe(obj){
            for(let item in obj){
                let dep = new __dep();
                let value = obj[item];
                if (Object.prototype.toString.call(value) === "[object Object]") __observe(value);
                Object.defineProperty(obj, item, {
                    configurable: true,
                    enumerable: true,
                    get: function reactiveGetter() {
                        if(__dep.target) dep.addSub(__dep.target);
                        return value;
                    },
                    set: function reactiveSetter(newVal) {
                        if (value === newVal) return value;
                        value = newVal;
                        dep.notifyAll();
                    }
                });
            }
            return obj;
        }

        this.$data = __observe(config.data);

        function __proxy (target) {
            for(let item in target){
                Object.defineProperty(this, item, {
                    configurable: true,
                    enumerable: true,
                    get: function proxyGetter() {
                        return this.$data[item];
                    },
                    set: function proxySetter(newVal) {
                        this.$data[item] = newVal;
                    }
                });
            }
        }

        __proxy.call(this, config.data);

        function __watcher(fn){
            this.update = function(){
                fn();
            }
            
            this.activeRun = function(){
                __dep.target = this;
                fn();
                __dep.target = null;
            }
            this.activeRun();
        }

        new __watcher(() => {
            console.log(this.msg, this.date);
        })

        new __watcher(() => {
            var html = String(this.__originHTML||'').replace(/"/g,'\\"').replace(/\s+|\r|\t|\n/g, ' ')
            .replace(/\{\{(.)*?\}\}/g, function(value){ 
                return  value.replace("{{",'"+(').replace("}}",')+"');
            })
            html = `var targetHTML = "${html}";return targetHTML;`;
            var parsedHTML = new Function(...Object.keys(this.$data), html)(...Object.values(this.$data));
            this.__root.innerHTML = parsedHTML;
        })

    }

    var vm = new Mvvm({
        el: "#app",
        data: {
            msg: "1",
            date: new Date(),
            obj: {
                a: 1,
                b: 11
            }
        }
    })

</script>
</html>

computed watcher

computed函數在自身內部維護的一個watcher,配合其內部的屬性dirty開關來決定computed的值是需要重新計算還是直接復用之前的值。
Vuecomputed是計算屬性,其會根據所依賴的數據動態顯示新的計算結果,雖然使用{{}}模板內的表達式非常便利,但是設計它們的初衷是用於簡單運算的,在模板中放入太多的邏輯會讓模板過重且難以維護,所以對於任何複雜邏輯,都應當使用計算屬性。計算屬性是基於數據的響應式依賴進行緩存的,只在相關響應式依賴發生改變時它們才會重新求值,也就是說只要計算屬性依賴的數據還沒有發生改變,多次訪問計算屬性會立即返回之前的計算結果,而不必再次執行函數,當然如果不希望使用緩存可以使用方法屬性並返回值即可,computed計算屬性非常適用於一個數據受多個數據影響以及需要對數據進行預處理的條件下使用。
computed計算屬性可以定義兩種方式的參數,{ [key: string]: Function | { get: Function, set: Function } },計算屬性直接定義在Vue實例中,所有gettersetterthis上下文自動地綁定為Vue實例,此外如果為一個計算屬性使用了箭頭函數,則this不會指向這個組件的實例,不過仍然可以將其實例作為函數的第一個參數來訪問,計算屬性的結果會被緩存,除非依賴的響應式property變化才會重新計算,注意如果某個依賴例如非響應式property在該實例範疇之外,則計算屬性是不會被更新的。

<!DOCTYPE html>
<html>
<head>
    <title>Vue</title>
</head>
<body>
    <div id="app"></div>
</body>
<script src="//cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: "#app",
        data: {
            a: 1,
            b: 2
        },
        template:`
            <div>
                <div>{{multiplication}}</div>
                <div>{{multiplication}}</div>
                <div>{{multiplication}}</div>
                <div>{{multiplicationArrow}}</div>
                <button @click="updateSetting">updateSetting</button>
            </div>
        `,
        computed:{
            multiplication: function(){
                console.log("a * b"); // 初始只打印一次 返回值被緩存
                return this.a * this.b;
            },
            multiplicationArrow: vm => vm.a * vm.b * 3, // 箭頭函數可以通過傳入的參數獲取當前實例
            setting: {
                get: function(){
                    console.log("a * b * 6");
                    return this.a * this.b * 6;
                },
                set: function(v){
                    console.log(`${v} -> a`);
                    this.a = v;
                }
            }
        },
        methods:{
            updateSetting: function(){ // 點擊按鈕後
                console.log(this.setting); // 12
                this.setting = 3; // 3 -> a
                console.log(this.setting); // 36
            }
        },

    })
</script>
</html>

whtcher api

watch api中可以定義deepimmediate屬性,分別為深度監聽watch和最初綁定即執行回調的定義,在render watch中定義數組的每一項由於性能與效果的折衷是不會直接被監聽的,但是使用deep就可以對其進行監聽,當然在Vue3中使用Proxy就不存在這個問題了,這原本是Js引擎的內部能力,攔截行為使用了一個能夠響應特定操作的函數,即通過Proxy去對一個對象進行代理之後,我們將得到一個和被代理對象幾乎完全一樣的對象,並且可以從底層實現對這個對象進行完全的監控。
對於watch api,類型{ [key: string]: string | Function | Object | Array },是一個對象,鍵是需要觀察的表達式,值是對應回調函數,值也可以是方法名,或者包含選項的對象,Vue實例將會在實例化時調用$watch(),遍歷watch對象的每一個property
不應該使用箭頭函數來定義watcher函數,例如searchQuery: newValue => this.updateAutocomplete(newValue),理由是箭頭函數綁定了父級作用域的上下文,所以this將不會按照期望指向Vue實例,this.updateAutocomplete將是undefined

<!DOCTYPE html>
<html>
<head>
    <title>Vue</title>
</head>
<body>
    <div id="app"></div>
</body>
<script src="//cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: "#app",
        data: {
            a: 1,
            b: 2,
            c: 3,
            d: {
                e: 4,
            },
            f: {
                g: 5
            }
        },
        template:`
            <div>
                <div>a: {{a}}</div>
                <div>b: {{b}}</div>
                <div>c: {{c}}</div>
                <div>d.e: {{d.e}}</div>
                <div>f.g: {{f.g}}</div>
                <button @click="updateA">updateA</button>
                <button @click="updateB">updateB</button>
                <button @click="updateC">updateC</button>
                <button @click="updateDE">updateDE</button>
                <button @click="updateFG">updateFG</button>
            </div>
        `,
        watch: {
            a: function(n, o){ // 普通watcher
                console.log("a", o, "->", n);
            },
            b: { // 可以指定immediate屬性
                handler: function(n, o){
                    console.log("b", o, "->", n);
                },
                immediate: true
            },
            c: [ // 逐單元執行
                function handler(n, o){
                    console.log("c1", o, "->", n);
                },{
                    handler: function(n, o){
                        console.log("c2", o, "->", n);
                    },
                    immediate: true
                }
            ],
            d: {
                handler: function(n, o){ // 因為是內部屬性值 更改不會執行
                    console.log("d.e1", o, "->", n);
                },
            },
            "d.e": { // 可以指定內部屬性的值
                handler: function(n, o){
                    console.log("d.e2", o, "->", n);
                }
            },
            f: { // 深度綁定內部屬性
                handler: function(n){
                    console.log("f.g", n.g);
                },
                deep: true
            }
        },
        methods:{
            updateA: function(){
                this.a = this.a * 2;
            },
            updateB: function(){
                this.b = this.b * 2;
            },
            updateC: function(){
                this.c = this.c * 2;
            },
            updateDE: function(){
                this.d.e = this.d.e * 2;
            },
            updateFG: function(){
                this.f.g = this.f.g * 2;
            }
        },

    })
</script>
</html>

每日一題

//github.com/WindrunnerMax/EveryDay

參考

//cn.vuejs.org/v2/api/#watch
//www.jianshu.com/p/0f00c58309b1
//juejin.cn/post/6844904128435470350
//juejin.cn/post/6844904128435453966
//juejin.cn/post/6844903600737484808
//segmentfault.com/a/1190000023196603
//blog.csdn.net/qq_32682301/article/details/105408261
Tags: