副作用:effect 函数的执行会直接或间接影响其他函数的执行。
响应式数据:当数据发生变化时,页面相应使用该数据的地方也会自动跟着发生变化。
响应式数据实现基本原理:使用 proxy 代理原始对象,从而拦截原始对象的读取和设置操作。在读取对象的属性时,定义一个数据桶,将副作用存放在这个数据桶中;在设置对象的属性时,遍历这个数据桶,将副作用取出执行。
第一版
const bucket = new Set()
const data = { text: 'hello world' }
const obj = new Proxy(data, {
get(target, key) {
bucket.add(effect)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
// test
function effect() {
document.body.innerText = obj.text
}
effect()
setTimeout(() => {
obj.text = 'hello vue3'
}, 1000);
目前出现的问题:硬编码了副作用函数的名字(effect),如果副作用函数的名字改变,那么上述代码就不能正常工作了。
解决思路:提供一个用来注册副作用函数的机制。让用户给副作用函数起什么名字都可以,甚至可以使用匿名函数。这样响应系统就不依赖副作用函数的名字了。
第二版
解决了响应系统依赖副作用函数的名字问题
const bucket = new Set()
const data = { text: 'hello world' }
let activeEffect // 新增
// 新增 start
function effect(fn) {
activeEffect = fn
fn()
}
// 新增 end
const obj = new Proxy(data, {
get(target, key) {
// 新增 start
if (activeEffect) {
bucket.add(activeEffect)
}
// 新增 end
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
// test
// 删除 start
// function effect() {
// document.body.innerText = obj.text
// }
// effect()
// 删除 end
effect(() => {
// effect 会被打印两次,effect 本身执行一次,后续 1000s 后由于出发了 set 操作又执行了一次
console.log('effect');
document.body.innerText = obj.text
})
setTimeout(() => {
obj.text = 'hello vue3'
}, 1000);
目前出现的问题:没有在副作用函数与被操作的目标字段之间建立明确的联系。例如当读取属性时,无论读取的是哪一个属性,其实都一样,都会把副作用函数收集到“桶”里;当设置属性时,无论设置的是哪一个属性,也都会把“桶”里的副作用函数取出并执行。副作用函数与被操作的字段之间没有明确的联系。
// effect 还是被打印了两次,但是合理的情况应该是不会触发第二次 effect 才对,因为我们并没有设置 obj.text。
// 我们读取了 obj.text,那么会收集 effect。但是我们并没有设置 obj.text,而是设置了一个对象上不存在的属性,但是第二次的 effect 依然触发了。
// 原因就是 effect 和被操作的字段之间并没有建立联系,所以就变成了不管哪个属性设置都会触发 effect。
// 我们要想办法让响应式数据和 effect 建立对应联系。
effect(() => {
console.log('effect');
document.body.innerText = obj.text;
});
setTimeout(() => {
obj.noExist = "hello vue3";
}, 1000);
解决思路:需要在副作用函数与被操作的字段之间建立联系,需要重新设置“桶”的数据结构。
effect(() => {
document.body.innerText = obj.text;
});
树形结构:target -> key -> effect
伪代码模拟数据结构:target 原始对象,里面包含一个个属性【成员】,每个属性包含自己的副作用函数。
const bucket = {
target1[depsMap]: {
key1[deps]: [effect1, effect2, ...],
key2[deps]: [effect1, effect2, ...]
},
target2[depsMap]: {
key1[deps]: [effect1, effect2, ...],
key2[deps]: [effect1, effect2, ...]
}
}
第三版
解决了被操作字段与副作用之间没有建立联系的问题
WeakMap 对 key 是弱引用,不影响垃圾回收器的工作。据这个特性可知,一旦 key 被垃圾回收器回收,那么对应的键和值就访问不到了。WeakMap 经常用于存储那些只有当 key 所引用的对象存在时(没有被回收)才有价值的信息,例如上面的场景中,如果 target 对象没有任何引用了,说明用户侧不再需要它了,这时垃圾回收器会完成回收任务。但如果使用 Map 来代替 WeakMap,那么即使用户侧的代码对 target 没有任何引用,这个 target 也不会被回收,最终可能导致内存溢出。
const bucket = new Set() // 删除
const bucket = new WeakMap() // 新增
const data = { text: 'hello world' }
let activeEffect
function effect(fn) {
activeEffect = fn
fn()
}
const obj = new Proxy(data, {
get(target, key) {
// 删除 start
// if (activeEffect) {
// bucket.add(activeEffect)
// }
// 删除 end
// 新增 start
if (!activeEffect) return target[key];
// 获取 key,为对应的 key 添加 对应的 effect
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);
// 新增 end
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
// bucket.forEach(fn => fn()) // 删除
// 新增 start
// 获取 target -> key -> effect
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
effects && effects.forEach((fn) => fn());
// 新增 end
return true
}
})
// test
effect(() => {
// 现在,只执行了一次 effect,达到了我们想要的效果
console.log('effect');
document.body.innerText = obj.text
})
setTimeout(() => {
obj.noExist = 'hello vue3' // 不触发
obj.text = 'hello vue3' // 触发
}, 1000);
封装 track 和 trigger 函数
let activeEffect
function effect(fn) {
activeEffect = fn
fn()
}
const data = { text: "hello world" }
const bucket = new WeakMap()
const obj = new Proxy(data, {
get(target, key) {
// 封装 track 函数
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
// 封装 trigger 函数
trigger(target, key)
}
})
function track(target, key) {
if (!activeEffect) return target[key]
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)
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
// test
effect(() => {
console.log("effect")
document.body.innerText = obj.text
})
setTimeout(() => {
obj.noExist = "hello vue3"
obj.text = "hello vue3"
}, 1000)