基於發佈-訂閱模式分析vue3.0響應式原理

什麼是發佈-訂閱模式

發佈-訂閱模式,網上找幾個文章吧:發佈訂閱模式Javascript設計模式之發佈-訂閱模式html

另一個類似的設計模式是觀察者模式,二者的區別能夠看看這篇文章:Observer vs Pub-Sub patternvue

三個重要概念

以下圖,能夠看到有三個主要元素react

  • 發佈者
  • 訂閱者
  • 消息中心
    發佈-訂閱過程

發佈和訂閱都是跟消息中心通訊,從而達到解耦。設計模式

vue3.0中的發佈訂閱

咱們來分析vue中是怎麼使用發佈-訂閱模式。緩存

首先了解下vue3.0中響應式的用法:reactive包裝數據,effect定義數據變化後的回調。bash

let counter = reactive({num: 0})
effect(() => {
    console.log(counter.num);
})
counter.num = 1; // 輸出1
複製代碼

reactive()爲目標對象建立一個Proxy對象(代理對象)。函數

function reactive(target) {
    return new Proxy(target, {
        get(target, key, receiver) {
            return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
            return Reflect.set(target, key, value, receiver);
        }
    })
}
複製代碼

響應式數據的發佈訂閱應該在何時呢?post

是否是應該在數據變化的時候發佈內容?數據變化在何時能獲取到呢?是的,在被賦值的時候,也就是set的時候。性能

經過源碼咱們知道,訂閱步驟放在了數據讀取的時候獲取,也就是effect(() => {console.log(counter.num);})默認執行的時候。ui

將訂閱函數track和發佈函數trigger加入代碼中。

function reactive(target) {
    return new Proxy(target, {
        get(target, key, receiver) {
            // 訂閱
            track(target, key);
            return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
            // 發佈
            trigger(target, key);
            return Reflect.set(target, key, value, receiver);
        }
    })
}
複製代碼

訂閱

訂閱的目的是將依賴存入targetMap(消息中心)。

let targetMap = new WeakMap();
track(target) {
    <!--容錯代碼-->
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        depsMap = new Map();
    }
    let deps = depsMap.get(key);
    if(!dep) {
        depsMAp = new Set();
    }
    
    // 將effect存入targetMap,這裏須要注意如何獲取到effect
    const effect = effect回調;
    if (!dep.has(effect)) {
        dep.add(effect);
    }
}
複製代碼

誒,但是如何獲取到effect成了一個問題。在vue源碼中,尤大大是在執行effect()時將effect依賴存到棧。當將effect被存到targetMap中後立刻回收掉該元素。 這裏須要看看effect的實現。

function effect(fn) {
    let effect = run(fn);
    effect();
    
    return effect;
}

let activeReactiveEffectStack = [];
function run(effect, fn) {
    try {
        activeReactiveEffectStack.push(effect);
        fn(...args);  
    }
    finally {
       activeReactiveEffectStack.pop(); 
    }
}
複製代碼

看完這段代碼,不少人都有兩個疑問: 爲何effect入棧後立刻就回收掉了?理論上執行一次effect都只會有一個effect,爲何要用棧的形式來緩存,用變量不就行了?

第一個問題,effect是在執行fn後纔回收的,fn就是() => {console.log(counter.num);}裏面的counter.num中執行了set()=>track()=>存依賴

第二個問題,我也不知道爲何,多是爲了之後的擴展吧

因此trackeffect能夠從activeReactiveEffectStack棧頂中獲取。

// 將effect存入targetMap,effect從棧頂中獲取。
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length -1];
複製代碼

到此,訂閱過程就完成啦。

發佈

上面提到發佈是在數據變化也就是set中觸發的。發佈過程很簡單,當偵聽到對應數據變化而且在targetMap中能找到相應的回調函數時,執行便可。

function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (depsMap) {
        cosnt deps = depsMap.get(key);
        deps.forEach(effect => effect());
    }
}
複製代碼

其餘

嵌套對象

在上面例子上作下修改。

let counter = reactive({num: 0, info: {from: 'program'});
effect(() => {console.log(counter.info.from)});
counter.info.from = 'book'; // 不會輸出結果
複製代碼

會發現嵌套的對象info並無被監聽到。這是由於Proxy只能監聽到一層,能夠對get作下修改:若是是對象,繼續用reactive包一層。

get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    track(target, key);
    return isObject(res) ? reactive(res) : res;
}
複製代碼

避免屢次代理

<!--同一個對象屢次代理-->
const origin = {num: 0}
let counter1 = reactive(origin)
let counter2 = reactive(origin)
let counter3 = reactive(origin)

<!--代理代理對象-->
let counter = reactive({num: 0})
let counter4 = reactive(counter)
let counter5 = reactive(counter4)
複製代碼

對同一個對象屢次代理,生成多個Proxy對象。對性能來講不太友好。代理過的對象再次代理是沒有必要的。

因此在vue3.0中對代理過的數據都進行了緩存。

  • rawToReactive : { raw => observed }源對象=>代理對象映射表。
  • reactiveToRaw: { observed => raw }代理對象=>源對象映射表。
<!--同一個對象屢次代理-->
const origin = {num: 0}
let counter1 = reactive(origin)
let counter2 = reactive(origin)
let counter3 = reactive(origin)

// 解決方式:在緩存中查找到代理對象,直接返回。
let observed = rawToReactive.get(target);
if (observed) {
    return observed;
}

<!--代理代理對象-->
let counter = reactive({num: 0})
let counter4 = reactive(counter)
let counter5 = reactive(counter4)

// 解決方式:若是是已經代理過的對象,直接返回。
if (reactiveToRaw.has(target)) {
return target
}
複製代碼

push等方法會屢次觸發set操做

function reactive(target) {
    return new Proxy(target, {
        get(target, key, receiver) {
            return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
            console.log(`set ${key} to ${value}`);
            return Reflect.set(target, key, value, receiver);
        }
    })
}
let proxy = reactive([1,2])
proxy.push(3);
// set 2 to 3
// set length to 3
複製代碼

上面的push操做會觸發兩次set操做,會形成兩次回調,這就是一個比較嚴重的問題。

其實在Reflect.set第一次執行時就將全部的數據設置正確了,第二次進入set的時候,length的值已是3了。因此能夠用新老值對比來過濾掉沒必要要的回調。

set(target, key, value, receiver) {
    const oldValue = target[key];
    // 新老值對比
    if (oldValue !== value) {
       console.log(`set ${key} to ${value}`);
    }
    return Reflect.set(target, key, value, receiver);
}
// set 2 to 3

複製代碼
相關文章
相關標籤/搜索