4.3 设计一个完善的响应式系统
关键词
响应系统的工作流程:
- 当读取操作发生时,将副作用函数收集到“桶”中。
- 当设置操作发生时,从“桶”中取出副作用函数并执行。
- activeEffect 全局变量当做中介存储副作用函数。
- 重新定义effect函数 =>变成了注册副作用函数的函数。
笔记
改善4.2缺陷逻辑
- 改善4.2中的缺陷,希望副作用函数的名字改变或者是匿名函数都能正常收集到Set存储桶中。因此需要提供一个注册副作用函数的机制。
- 副作用函数已存储到activeEffect中,所以get拦截函数应该把activeEffect收集到Set存储桶中,如此响应式系统就不依赖副作用函数的名字了。
// 用一个全局变量存储被注册的副作用函数
let activeEffect
// effect 函数用于注册副作用函数
// 参数fn 就是将要注册的副作用函数
function effect(fn) {
// 当调用effect 注册副作用函数时,将副作用函数fn 赋值给actvieEffect
activeEffect = fn
// 执行副作用函数
fn()
}
// 使用一个匿名的副作用函数作为 effect 函数的参数
// 首先会把匿名的副作用函数 fn 赋值给全局变量 activeEffect
// 接着执行被注册的匿名副作用函数 fn,会触发响应式数据 obj.text 的读取操作,进而触发代理对象 Proxy 的 get 拦截函数
effect(
// 一个匿名的副作用函数
() => {
document.body.innerText = obj.text
}
)
代理对象改动
// 代理对象 Proxy
const obj = new Proxy(data, {
get(target, key) {
// 将 activeEffect 中存储的副作用函数收集到“桶”中
if (activeEffect) { // 新增
bucket.add(activeEffect) // 新增
} // 新增
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
修改后存在问题 :不存在属性依然执行
当我们修改响应式数据obj 不存在的一个属性的时候,匿名副作用函数依然会执行
effect(
// 一个匿名的副作用函数
() => {
console.log('effct run') // 会打印两次
document.body.innerText = obj.text;
}
);
setTimeout(()=>{
obj.noExist = 'hello vue3'
})
完整代码 demo (如下)
const bucket = new Set();
const data = { text: "hello world" };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
// 参数fn 就是将要注册的副作用函数
function effect(fn) {
// 当调用effect 注册副作用函数时,将副作用函数fn 赋值给actvieEffect
activeEffect = fn;
// 执行副作用函数
fn();
}
// 代理对象 Proxy
const obj = new Proxy(data, {
get(target, key) {
// 将 activeEffect 中存储的副作用函数收集到“桶”中
if (activeEffect) {// 新增
bucket.add(activeEffect); // 新增
} // 新增
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
bucket.forEach((fn) => fn());
return true;
}
});
// 使用一个匿名的副作用函数作为 effect 函数的参数
// 首先会把匿名的副作用函数 fn 赋值给全局变量 activeEffect
// 接着执行被注册的匿名副作用函数 fn,会触发响应式数据 obj.text 的读取操作,进而触发代理对象 Proxy 的 get 拦截函数
effect(
// 一个匿名的副作用函数
() => {
console.log('effct run') // 会打印两次
document.body.innerText = obj.text;
}
);
setTimeout(()=>{
obj.noExist = 'hello vue3'
})
根本原因:没有在副作用函数与被操作的目标字段之间建立明确的联系
也就是说,当我们读取或者设置时,无论操作哪一个属性,那么get和set方法都会执行
解决方案: 修改数据结构
观察代码,并思考如何设计数据结构。(树型结构)
effect(function effectFn() {
document.body.innerText = obj.text
})
在这段代码中存在三个角色:
- 被操作(读取)的代理对象 obj
- 被操作(读取)的字段名 text
- 使用 effect 函数注册的副作用函数 effectFn
树形结构(三种情况举例)
使用WeakMap 代替Set作为桶的数据结构
// 存储副作用函数的桶
const bucket = new WeakMap()
修改get/set 拦截器
const obj = new Proxy(data,{
get(target,key){
// 没有activeEffect直接return
if(!activeEffect){
return target[key]
}
// 根据target 从WeakMap桶中取得depsMap,它也是一个Map类型:key-->effects
let depsMap = bucket.get(target)
// 如果不存在depsMap 则新建一个Map与target关联
if(!depsMap) {
bucket.set(target,(depsMap = new Map()))
}
// 根据key 从 depsMap 中取得deps,它是一个Set类型
// 里面存储这所有与当前Key相关联的副作用函数effects
let deps = depsMap.get(key)
// 如果deps不存在 同样新建一个Set并关联
if(!deps) {
depsMap.set(key,(deps = new Set()))
}
// 最后将当前激活的副作用函数添加到WeakMap 桶里
deps.add(activeEffect)
// 返回属性值
return target[key]
},
set(target,key,newVal){
// 设置属性值
target[key] = newVal
// 根据target 从WeakMap桶中取得depsMap 它是key -->effects
const depsMap = bucket.get(target)
if(!depsMap) { return }
//根据key 获取所有副作用函数effects
const effects = depsMap.get(key)
// 执行副作用函数
effects && effects.forEach(fn=>fn())
}
})
WeakMap、Map 和 Set 之间的关系
为什么要使用 WeakMap(WeakMap与Map的区别)
WeakMap 对 key 是弱引用,不影响垃圾回收机制,不会造成内存泄露,自动回收。
立即执行函数执行完毕后,Map的key foo 依然保持着对 foo 对象的引用,而WeakMap 的key bar 属性就不能访问了。
更多参见底部延伸阅读
const map = new Map();
const weakmap = new WeakMap();
(function(){
const foo = {foo: 1};
const bar = {bar: 2};
map.set(foo, 1);
weakmap.set(bar, 2);
})()
优化代码 封装内部逻辑 trigger 触发 track 追踪
代码优化,封装get和set内把副作用函数放入桶内和执行副作用函数的逻辑
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)
}
})
// 在 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())
}