Vue中的三種Watcher

WindrunnerMax發表於2021-06-08

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="https://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="https://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>

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

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

相關文章