Vue3 响应式原理探索Part 2 - 多重结构的响应式

390 阅读3分钟

“这是我参与更文挑战的第4天,活动详情查看: 更文挑战

上文《Vue3 响应式原理探索Part 1 - 20行代码实现响应式》,我们介绍了什么是响应式,以及一个弱鸡版本的实现。并提出了一个疑问,我们这篇来解决它。

参考 www.vuemastery.com/courses/vue…

多个属性如何创建独立的响应式

let param = { width: 4, height: 5 };
let size = 0;

let effect = () => {
    size = param.width * param.height
}
...

我们的 param 有两个属性 width | height ,他们都分别被其他变量依赖着,譬如:

let param = { width: 4, height: 5 };

let widthNew = param.width * 2;
let sizeNew = Math.PI * param.width * param.width;
let heightNew = param.height * 3;

可以看到 width 分别被 widthNew sizeNew 依赖,heightheightNew 依赖。当 widthheight 更新时,他们相应的依赖需要自动更新。但我们如何做到呢。

Map 是ES6引进的一个类型,专门用来存储键值型的数据,可以通过 key 设置(set) 或 获取(get) 相应的 value。我们可以考虑将 widthheight 存储做为 key, 将他们相应的依赖作为值存储进 value


const depsMap = new Map();
function track(key) {
    let dep = depsMap.get(key);
    if (!dep) {
        depsMap.set(key, (dep = new Set));
    }
    dep.add(effect);
}
function trigger(key) {
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach(effect => {
            effect();
        });
    }
}
let param = { width: 5, height: 2 }
let size = 0;
let effect = () => {
    size = param.width * param.height;
};

track('height');
track('width');
effect();
console.log(size); // =>  10

param.height = 3;
trigger('height');
console.log(size); // => 15


param.width = 6;
console.log(size); // => 15,没有手动触发 'width' 的依赖,所以还是15
trigger('width');
console.log(size); // => 18

以上的实现可以看出,我们只要在相应的属性变化时,手动执行相应的 trigger 即可实现简单的响应式。

多个响应式对象

如果有多个不同的param对象需要创建响应式,存储结构又该如何调整呢。毕竟现实的工程项目中,不可能仅仅只有一个对象。

我们可以考虑再做一层数据结构的封装,将上述实现的每个对象的依赖 depsMap 都做为 value,但问题来了,用什么作为 key 呢。

还好 ES6 提供了另外一个结构, WeakMap

WeakMap 能够将对象做为自己的 key ,而且它只能将对象做为 key。 Amazing 。

let product = { width: 5, height: 2 }
const targetMap = new WeakMap()
targetMap.set(product, "example code to test")
console.log(targetMap.get(product)) // ---> "example code to test"

我们将 WeakMap 的实例命名为 targetMap, 调整后的存储结构如下图,图中的 product quantity price total 替换为我们示例中的 param width height size 即可理解。

image.png

当我们创建依赖或者触发的时候,我们需要将相应的对象作为参数传入,这样才知道应该去响应这个对象。具体实现如下:

const targetMap = new WeakMap();
function track(target, key) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
        depsMap.set(key, (dep = new Set()));
    }
    dep.add(effect);
}
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach(element => {
            element();
        });
    }
}
let param = { width: 5, height: 2 };
let size = 0;
let effect = () => {
    size = param.width * param.height;
}
track(param, 'height');
track(param, 'width');
effect();
console.log(size); // 10;

param.height = 3;
trigger(param, 'height');
console.log(size); // 15

param.width = 6;
trigger(param, 'width');
console.log(size); // 18

通过上述实现即可有效的追踪多个对象的依赖,在创建一个响应式系统时,它是一个重要的前菜。

新的疑问

但我们如何避免弱鸡的手动的 trigger 函数,而自动触发呢。另外,我们能不手动的 track 吗。

答案是可以的。

我们可以先学习

然后就可以基于 Proxy + Reflect,用 Vue3 响应式原理探索- Part 3 Proxy + Reflect + activeEffect 实现自动响应式了。