發佈-訂閱模式,網上找幾個文章吧:發佈訂閱模式、Javascript設計模式之發佈-訂閱模式html
另一個類似的設計模式是觀察者模式,二者的區別能夠看看這篇文章:Observer vs Pub-Sub patternvue
以下圖,能夠看到有三個主要元素react
發佈和訂閱都是跟消息中心通訊,從而達到解耦。設計模式
咱們來分析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()=>存依賴
。
第二個問題,我也不知道爲何,多是爲了之後的擴展吧
因此track
中effect
能夠從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
}
複製代碼
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
複製代碼