什么是发布-订阅模式
发布-订阅模式,网上找几个文章吧:发布订阅模式、Javascript设计模式之发布-订阅模式
另外一个相似的设计模式是观察者模式,两者的区别可以看看这篇文章:Observer vs Pub-Sub pattern
三个重要概念
如下图,可以看到有三个主要元素
- 发布者
- 订阅者
- 消息中心
发布和订阅都是跟消息中心通信,从而达到解耦。
vue3.0中的发布订阅
我们来分析vue中是怎么使用发布-订阅模式。
首先了解下vue3.0中响应式的用法:reactive包装数据,effect定义数据变化后的回调。
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);
}
})
}
响应式数据的发布和订阅应该在什么时候呢?
是不是应该在数据变化的时候发布内容?数据变化在什么时候能获取到呢?是的,在被赋值的时候,也就是set的时候。
通过源码我们知道,订阅步骤放在了数据读取的时候获取,也就是effect(() => {console.log(counter.num);})默认执行的时候。
将订阅函数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) {
const result = Reflect.set(target, key, value, receiver);
// 发布
trigger(target, key);
return result;
}
})
}
订阅
订阅的目的是将依赖存入targetMap(消息中心)。
let targetMap = new WeakMap();
track(target) {
<!--容错代码-->
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, depsMap = new Map());
}
let deps = depsMap.get(key);
if(!deps) {
depsMap.set(key, deps = new Set());
}
// 将effect存入targetMap,这里需要注意如何获取到effect
const activeEffect = effect回调;
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
}
}
诶,可是如何获取到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);
activeEffect = fn;
}
finally {
activeReactiveEffectStack.pop();
activeEffect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1];
}
}
看完这段代码,很多人都有两个疑问: 为什么effect入栈后马上就回收掉了?理论上执行一次effect都只会有一个effect,为什么要用栈的形式来缓存,用变量不就好了?
第一个问题,effect是在执行fn后才回收的,fn就是
() => {console.log(counter.num);}里面的counter.num中执行了set()=>track()=>存依赖。
第二个问题,为了effect嵌套时,当前activeEffect拿到正确的effect
所以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
}
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