我们都知道,vue3.x 是采用 Proxy 实现响应数据的,本篇文章也将围绕 vue3.x 的响应机制开始,展开对响应式数据和副作用函数的实现
副作用函数
副作用函数就是指有副作用的函数,例如下面例子:
const obj = { text: 'hello world' }; // 全局变量 obj
function effect() {
obj.text = 'hello vue';
}
function elseFn() {
console.log(obj.text); // 本来是打印 hello world 的,但是被effect影响了
}
在 effect 函数 执行时,会设置 obj.text,但除了 effect 函数之外,其他函数也可以读取或者设置 obj.text,也就是说 effect 函数会影响其他函数的执行。这时我们就可以认为 effect 产生了副作用。
响应式数据
了解了什么叫副作用函数,那么再看看什么是响应式数据,假设某个副作用函数读取了对象的属性:
const obj = { text: 'hello world' };
function effect() {
console.log(obj.text);
}
effect()
如上面所示,effect 执行的时候会读取 obj.text,打印 hello world,当 obj.text 发生变化的时候,我们希望 effect 重新执行:
obj.text = 'hello vue'; // 修改text,同时希望effect重新执行
这句代码修改了 obj.text 的值,我们希望当值变化的时候对应的副作用函数能够随之执行并且打印 hello vue,如果能够执行,那么就可以称 obj 为响应式数据了,但是从目前代码来看还不能做到这点,因为 obj 仅仅是一个普通对象。接下来看看如何实现响应式数据的
响应式数据基本实现
接着上文思考,如何将 obj 成为响应式数据,了解过 vue2 的人都知道,vue2 的响应式数据是用 Object.defineProperty 拦截 get 和 set 的,那么一样的,最主要的就是要拦截到对象的读取和设置,vue3 采用 ES2015+ 的 Proxy 代替 Object.defineProperty 拦截,关于 Proxy 如何代理可以阅读相关文章: {% post_link 知识点/Proxy对象 Proxy对象 %}
思路是这样的:
- 当读取
obj.text时,将副作用函数effect收集到一个'桶'里 - 当设置
obj.text时,将副作用函数effect从'桶'里取出执行
思路有了,那么开始写代码:
const bucket = new Set();
const obj = { text: 'hello world' };
const proxy = new Proxy(obj, {
get(target, key) {
bucket.add(effect);
return target[key]
},
set(target, key, newValue) {
target[key] = newValue;
bucket.forEach(fn => fn());
return true;
}
})
function effect() {
console.log(proxy.text);
}
effect(); // 第一次执行打印 hello world
proxy.text = 'hello vue'; // 修改后 effect第二次执行打印 hello vue
解释一下这段代码,obj 利用 Proxy 创建了一个代理对象 proxy,在 effect 第一次执行的时候会读取代理对象属性 proxy.text ,拦截函数 get 从而收集 effect 副作用函数,当执行到 proxy.text = 'hello vue' 时,拦截函数 set 将收集到的副作用函数 effect 取出并执行,并打印出修改后的 proxy.text,这样我们就完成了一个简单的响应式系统了,当然还有很多要完善的地方,下面再继续完善。
完善响应式系统
上文中之所以说还不够完善,因为它有很多缺点,比如 如果副作用函数不叫 effect 了,那么就需要修改 get() 函数里的代码。想办法优化该代码:
let activeEffect = null;
const bucket = new Set();
const obj = { text: 'hello world', name: 'foo' };
function effect(fn) {
activeEffect = fn;
fn();
}
const proxy = new Proxy(obj, {
get(target, key) {
if (activeEffect) {
bucket.add(activeEffect);
}
return target[key]
},
set(target, key, newValue) {
target[key] = newValue;
bucket.forEach(fn => fn());
return true;
}
})
effect(() => {
console.log(proxy.text);
});
proxy.text = 'hello vue';
从上面代码看,增加了 activeEffect 全局变量来存放副作用函数,提供 effect 函数来注册副作用函数,当 effect 执行时,fn 存放到 activeEffect 中,接着执行 fn,读取 proxy.text , get() 收集副作用函数到'桶'里,这样 effect 可以执行多次并且与函数名无关,就解决了硬编码问题。但是仔细思考,还是有缺陷,比如,去修改一个 proxy 不存在或副作用函数没有读取到的属性:
proxy.noExist = '';
// proxy.name = 'bar';
执行上面的代码可以发现:
effect(() => {
console.log(proxy.text);
});
这段代码也执行了,明明副作用函数读取的属性并没有包括 noExist 或 name,却可以触发匿名副作用函数,这明显不是我们想要的。可以看出匿名副作用函数并没有读取 noExist,所以理论上 proxy.noExist 即使发生变化,匿名副作用函数也不会重新执行。这就要回到我们所谓的 '桶' 的数据结构了。原因是读取的属性并没有和副作用函数联系在一起,改进一下 bucket 的数据结构,如图:
bucket 数据结构
代码实现:
let activeEffect = null;
const bucket = new Map();
const obj = { text: 'hello world', name: 'foo' };
function effect(fn) {
activeEffect = fn;
fn();
}
const proxy = new Proxy(obj, {
get(target, key) {
if (!activeEffect) {
return target[key];
}
let effectFns = bucket.get(key);
if (!effectFns) {
bucket.set(key, (effectFns = new Set()))
}
effectFns.add(activeEffect);
return target[key];
},
set(target, key, newValue) {
target[key] = newValue;
const effectFns = bucket.get(key);
effectFns && effectFns.forEach(fn => fn());
return true;
}
})
上面代码我们将 bucket 的数据结构改成了 Map,让它可以记录每个属性所对应的副作用函数,接下来看看不同属性和副作用函数执行情况:
effect(() => {
console.log(proxy.text);
});
proxy.text = 'hello vue'; // 正常执行匿名副作用函数
proxy.name = 'bar'; // 不执行
proxy.noExist = ''; // 不执行
这样我们就完成了属性和副作用函数之间的联系了,但是紧接着还有问题,不同对象之间,属性名可能相同,以现在的数据结构很明显还有缺陷,我们再继续改进数据结构,如图:
bucket 数据结构
上图所示,我们增加了 WeakMap 一层数据结构,target 代表着被代理的对象。这样就很明确了,代码实现如下:
let activeEffect = null;
const bucket = new WeakMap();
const obj = { text: 'hello world', name: 'foo' };
function effect(fn) {
activeEffect = fn;
fn();
}
const proxy = new Proxy(obj, {
get(target, key) {
if (!activeEffect) {
return target[key];
}
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let effectFns = depsMap.get(key);
if (!effectFns) {
depsMap.set(key, (effectFns = new Set()))
}
effectFns.add(activeEffect);
return target[key];
},
set(target, key, newValue) {
target[key] = newValue;
const depsMap = bucket.get(target);
if (depsMap) {
const effectFns = depsMap.get(key);
effectFns && effectFns.forEach(fn => fn());
}
return true;
}
})
const proxy1 = new Proxy({text: 1}, ....);
const proxy2 = new Proxy({text: 2}, ....);
effect(() => {
console.log(proxy1.text);
});
effect(() => {
console.log(proxy2.text);
});
proxy1.text = 'hello vue'; // 不影响 proxy2.text 的副作用函数
proxy2.text = 'hello vue'; // 不影响 proxy1.text 的副作用函数
这样,proxy1 proxy2 就互不相干了,我们就可以去代理不同对象具有相同属性的情况了。分析上面的代码,最终,我们将 bucket 的数据结构改成了 WeackMap -> Map -> Set,当 访问 proxy.text 时,会先从 bucket 的键寻找原对象(obj),不存在则手动添加,否则取出键为 obj 的值,再从取出的值找到 key 为 text 的副作用函数集合,将当前活跃的副作用函数 activeEffect 加入这个集合中。当这个代理对象有设置操作时,经过 set 拦截,若 key 刚好为 text ,那么就会从 bucket 找到相对应的副作用函数集合并执行。
bucket 数据结构中出现了 WeakMap,那么为什么要用 WeakMap 而不用 Map ,两者有什么区别?
先看一段代码:
const weakMap = new WeakMap();
const map = new Map();
(function() {
const bar = {text: 1};
const foo = {text: 1};
weakMap.set(bar, 1);
map.set(foo, 2);
})()
我们都知道,js引擎的垃圾回收会根据数据的引用次数来回收,引用次数不为0就不回收,上面代码自执行函数执行后,foo 它没有被回收,因为它被 map 作为 key 引用着,导致没办法回收。而 bar 则可以被回收,这是因为,WeakMap 是弱引用,顾名思义,不计垃圾回收引用次数,也就是没有引用次数,垃圾回收器可以把 bar 的内存回收掉。
那回到我们的响应式系统,将 target (需要被代理的对象) 作为 WeakMap 的 key 意义何在?
它的价值所在其实就体现在,这个对象何时是需要响应式的,当用户侧没有引用时,那么它就不需要了。可以想象,我们打开一个弹窗组件,这个时候组件相当于一个闭包,内部的对象正好作为 WeakMap 的 key ,那么当弹窗销毁之后,组件内部的对象已经不需要响应式了,正好这个对象被垃圾回收器回收。如果换成 Map 那么会有内存泄漏的情况
最后我们为了代码的简洁,将 get() 和 set()中收集和执行副作用函数的代码封装一下,完整代码如下:
function track(target, key) {
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let effectFns = depsMap.get(key);
if (!effectFns) {
depsMap.set(key, (effectFns = new Set()))
}
effectFns.add(activeEffect);
}
function trigger(tartget, key) {
const depsMap = bucket.get(target);
if (depsMap) {
const effectFns = depsMap.get(key);
effectFns && effectFns.forEach(fn => fn());
}
}
let activeEffect = null;
const bucket = new WeakMap();
function effect(fn) {
activeEffect = fn;
fn();
}
const proxy = new Proxy(obj, {
get(target, key) {
if (!activeEffect) {
return target[key];
}
track(target, key);
return target[key];
},
set(target, key, newValue) {
target[key] = newValue;
trigger(target, key);
return true;
}
})
查看原文: 响应系统的作用与实现-1