阅读 1101

基于发布-订阅模式分析vue3.0响应式原理

什么是发布-订阅模式

发布-订阅模式,网上找几个文章吧:发布订阅模式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

所以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

复制代码
文章分类
前端
文章标签