文章内容来源:《Vue.js设计与实现》 —— 当当网 ,作者:霍春阳(HcySunYang)
一、通过 Proxy 实现基本的响应式数据:
function Section1 () {
// 存储副作用函数的桶
const bucket = new Set();
// 原始数据
const data = {text: "hello world"}
// 对原始数据的代理
let obj = new Proxy(data, {
// 拦截读取操作
get (target, key) {
// 将副作用函数 effect 添加到存储副作用的桶中
bucket.add(effect);
// 返回属性值
return target[key];
},
set (target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 将副作用函数从桶里取出并执行
bucket.forEach(fn => fn());
// 返回 true 代表设置操作成功
return true;
}
});
// 副作用函数
function effect () {
document.body.innerText = obj.text;
}
effect ();
setTimeout(() => {
obj.text = 'hello vue3'
}, 1000);
// 旧 ↑↑↑↑
}
Section1();
二、设计一个较完善的响应式系统
/**
* 解决 一、中 effect 副作用函数是硬编码命名函数的情况
* 提供一个注册副作用函数的机制
*/
function Section2 () {
// 存储副作用函数的桶
const bucket = new Set();
// 原始数据
const data = {text: "hello world"}
// 用一个全局变量存储被注册的的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
function effect(fn) {
// 当调用effect注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}
/**
* 重写 obj 的 Proxy
*/
const obj = new Proxy(data, {
get (target, key) {
// 如果 activeEffect 存在(副作用函数赋值给了activeEffect),就将 activeEffect 存储的副作用函数收集到“桶”里
if (activeEffect) { // 比旧obj的proxy 新增
bucket.add(activeEffect); // 比旧obj的proxy 新增
} // 比旧obj的proxy 新增
// 返回属性值
return target[key];
},
set (target, key, newVal) {
target[key] = newVal;
bucket.forEach(fn => fn());
return true;
}
});
/**
* 如何使用 effect 函数?
*/
// 1、匿名函数
effect (
// 一个匿名的副作用函数
() => {
document.body.innerText = obj.text;
}
)
// 2、具名函数
function setBodyInnerText () {
document.body.innerText = obj.text;
}
effect(setBodyInnerText);
setTimeout(() => {
obj.text = 'hello Section2'
}, 2000);
// 以上 ↑↑↑↑ 将 副作用函数存储到activeEffect中,在把activeEffect收集到 “桶”里,响应系统不再依赖副作用函数的名字了
/**
* 由于上面没有在副作用函数和被操作的目标字段之间建立明确的联系,
* 如果调用 obj 不存在的属性例如(obj.notExit),与obj.text相关的副作用也会执行,这是不正确的。
*/
effect(
// 匿名函数
() => {
console.log('effect run'); // 会打印两次
document.body.innerText = obj.text;
}
);
setTimeout(() => {
// 副作用函数中并没有读取 notExit 属性的值 ↑↑↑
obj.notExit = 'hello Vue3'
}, 1000);
}
// Section2();
function Section3 () {
// 原始数据
const data = {text: "hello world"}
/**
* 解决 副作用函数 和 被操作的目标字段 之间建立明确联系的问题。
* target:表示一个代理对象(Proxy)所代理的原始对象
* key:表示被操作的字段名
* effectFn:表示被注册的副作用函数
* 以上3个角色的关系如下:
* target
* |__ key
* |__ effectFn
*
* 2、如果有2个副作用函数同时读取同一个对象的属性值:
* effect(function effectFn () { obj.text; });
* effect(function effectFn2 () { obj.text; });
* 那么关系如下:
* target
* |__ text
* |__ effectFn
* |__ effectFn2
*
* 3、如果一个副作用函数中读取了同一个对象的2个不同属性:
* effect (function effectFn () {
* obj.text;
* obj.text2;
* });
* 那么关系如下:
* target
* |__ text
* |__ effectFn
* |__ text2
* |__ effectFn
*
* 4、如果在不同副作用函数中,读取了2个不同对象的不同属性:
* effect (function effectFn () {
* obj1.text1;
* });
* effect (function effectFn2 () {
* obj2.text2;
* });
* 那么关系如下:
* target
* |__ text
* |__ effectFn
*
* target2
* |__ text2
* |__ effectFn2
*
* target 对应 n 个 key, 每个 key 又对应了 n 个 effectFn,
* 为了保证一一对应关系,需要用 WeakMap、Map 和 Set 将 target、key、effectFn 关联起来
*/
// 实现新的“桶”,首先使用 WeakMap 替代 Set 作为桶的数据结构:
// 存储副作用函数的桶
const bucket = new WeakMap();
// 用一个全局变量存储被注册的的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
function effect(fn) {
// 当调用effect注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}
// 修改 get / set 拦截器代码:
const obj = new Proxy(data, {
// 拦截读取操作
get (target, key) {
// 没有 activeEffect ,直接 return 属性值
if (!activeEffect) return target[key];
// 1、根据 target 从“桶”中取得 depsMap, 是 target 对应的 Map,Map 包含了 target 各个 key,以及不同 key 对应的不同 effectFn
let depsMap = bucket.get(target);
// 如果 depsMap 不存在,就新建一个 Map 与 target 关联
if (!depsMap) {
// 这里的Map,后续用来存储 target 的 key
bucket.set(target, (depsMap = new Map()));
}
// 2、再根据 key 从 depsMap 中取得 deps, deps是一个 Set 类型,里面存储着所有与当前 key 相关联的副作用函数:effects
let deps = depsMap.get(key);
// 如果 deps 不存在,就新建一个 Set 与 key 关联
if (!deps) {
// 这里的 Set,后续用于存储与 key 关联的副作用函数 effects
depsMap.set(key, (deps = new Set()));
}
// 3、最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect);
// 返回属性值
return target[key];
},
// 拦截设置操作
set (target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 根据 target 从桶中取出 depsMap,depsMap如果存在,就是Map数据,是 key ----> effects 对应关系
let depsMap = bucket.get(target);
// 如果 depsMap 不存在,就不执行后续操作
if (!depsMap) return
// 根据 key 从 depsMap 取出 effects , effects 是 Set 数据,存储的是所有与 key 关联的副作用函数
let effects = depsMap.get(key);
// 如果存在关联副作用函数,执行每一个副作用函数
effects && effects.forEach(fn => fn());
}
});
effect(
// 匿名函数
() => {
console.log('effect run'); // 只打印1次,已确立 effect 和 key 的明确联系
document.body.innerText = obj.text;
}
);
setTimeout(() => {
// 副作用函数中并没有读取 notExit 属性的值 ↑↑↑
obj.notExit = 'hello Section3'
}, 1000);
}
// Section3();
function Section4 () {
/**
* 为何 target 和 key 的关系要使用 WeakMap ?
* 1、WeakMap 是弱引用,Map 是强引用。
* 2、WeakMap 的 key 是弱引用,在 WeakMap 的表达式执行完之后,垃圾回收器(grabage collector)就会将对象 target 从内存中移除。
* 3、Map 的 key 是强引用,在 Map 的表达式执行完之后,对于对象 target 来说,它仍然作为 Map 的 key 被引用着,因此垃圾回收器不会把它从内存中移除。
* 举例:
*/
// 创建 Map 数据
const mapData = new Map();
// 创建 WeakMap 数据
const weakmapData = new WeakMap();
// IIFE 立即执行函数,执行表达式
(function() {
const key1 = { foo: 1 };
const key2 = { bar: 2 };
// Map 数据执行表达式,在此处引用 key1 对象作为 mapData 的 key
mapData.set(key1, 1);
// WeakMap 数据执行表达式,在此处引用 key2 对象作为 weakmapData 的 key
weakmapData.set(key2, 2);
// 此处已执行完表达式
console.log('mapData.keys :>> ', mapData.keys);
// 打印:mapData.keys :>> ƒ keys() { [native code] }
for (const k of mapData.keys()) {
console.log('k :>> ', k);
// 打印:k :>> {foo: 1}
};
console.log('weakmapData.keys :>> ', weakmapData.keys);
// 打印:weakmapData.keys :>> undefined
console.log('weakmapData :>> ', weakmapData);
// 打印:weakmapData :>> WeakMap {{…} => 2}
console.log('weakmapData.get(key2) :>> ', weakmapData.get(key2));
// 打印:weakmapData.get(key2) :>> 2
/**
* 执行完 mapData.set() 和 weakmapData.set() 后,
* 1、Map数据:对于 key1 对象,它仍然作为 mapData 的 key 被引用着,不会被垃圾回收器从内存中移除,可通过 mapData.keys 打印出 key1 对象。
* 2、WeakMap 数据:对于 key2 对象,由于 WeakMap 的 key 是弱引用,它不影响垃圾回收器的工作,在.set()表达式执行完之后,垃圾回收器就会将 key2 对象从内存中移除,并且我们无法通过 weakmapData 的 key 值,也就无法通过 weakmapData 取得 key2 对象。
* 3、简单说:WeakMap 对 key 是弱引用,不影响垃圾回收器的工作。根据这个特性可知:一旦 key 被垃圾回收器回收,那么对应的键和值就访问不到了。
* 4、WeakMap 应用场景:用于存储那些只有当 key 所引用的对象存在时(没有被回收)才有价值的信息。
* 例如上面的场景,如果 key2 对象没有任何引用了,说明用户侧不再需要它,这时垃圾回收器就会完成回收任务。如果用 Map 代替 WeakMap ,那么即使用户侧对 key2 没有任何引用,这个 key2 也不会被回收,最终可能导致内存溢出。
*/
})()
}
// Section4();
/**
* 第5节 封装处理
* 1、将 get 拦截函数里 编写副作用函数收集到 “桶” 里的逻辑,单独封装到一个 track 函数中,表示追踪。
* 2、将 set 拦截函数里 触发副作用函数重新执行的逻辑,单独封装到一个 trigger 函数中,表示触发。
*/
function Section5 () {
// 原始数据
const data = { text: 'hello world' };
// 存储副作用函数的桶
const bucket = new WeakMap();
// 用一个全局变量存储被注册的的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
function effect(fn) {
// 当调用effect注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}
// 在 get 拦截函数内调用 track 函数追踪变化
function track (target, key) {
// 没有 activeEffect,直接 return 属性值
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
};
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()))
};
deps.add(activeEffect);
}
// 在 set 拦截函数内调用 trigger 函数触发副作用函数
function trigger (target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
effects && effects.forEach(fn => fn());
}
const obj = new Proxy(data, {
// 拦截读取操作
get (target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return target[key];
},
// 拦截设置操作
set (target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
trigger (target, key);
}
})
effect(
// 匿名函数
() => {
console.log('effect run'); // 只打印1次,已确立 effect 和 key 的明确联系
document.body.innerText = obj.text;
}
);
effect(
// 匿名函数
() => {
// 打印2次,设置 obj.notExit 时触发打印
console.log('obj.notExit:>> ', obj.notExit);
}
);
setTimeout(() => {
// 副作用函数中并没有读取 notExit 属性的值 ↑↑↑
obj.notExit = 'hello Section5'
}, 1000);
}
Section5();