概述
今天我们研究副作用函数,有人提出疑问:副作用函数是个啥?
举例:小明有十个暗恋对象,十个人都是未婚,那她们每个人都有可能和小明结婚。有一天,其中一个名叫刘大漂亮嫁给隔壁村的王二狗。刘大漂亮和王二狗结婚没有对小明造成直接影响,但小明能结婚的选择却少了一个。
代码举例: 我们把id为name的dom内容改成了'我是王二狗',那么所有使用到它的地方获取到的值都会发生变化。
const dom = document.getElementById('name');
dom.innerHTML = '我是王二狗'
effect副作用函数
effect和watch本质上是一样的,今天我们用vue3种的effect举例,搞起。
最简单的effect
声明一个对象,当对象中的内容发生变化时,就触发一个函数。
function effectFn() {
console.log('effectFn被触发了')
}
const data = {
ok: true,
text: '哈哈哈'
}
const proxyData = new Proxy(data, {
get(target, key) {
return target[key]
},
set(target, key, newVal) {
target[key] = newVal;
effectFn()
}
})
proxyData.ok = false
proxyData.text = 20
proxyData.text = 22
proxyData.text = 23
上面的例子很简单,问题也很多:
- effectFn是固定的。
- 对象的key并没有触发使用过自己的函数,只能触发同一个函数。
动态的effect
下面我们解决这个问题,先看副作用响应系统设计图:
- 利用WeakMap以target对象为key,Map为值。
- 再已key为Map的key,set为值。这样每个key都能存储到使用过自己的函数。
- 当自己被修改时,把储存在set中的函数全部执行一遍。
let activeEffect;
function effect(fn) {
activeEffect = fn;
fn()
}
const data = {
ok: true,
text: '哈哈哈'
}
const bucket = new WeakMap()
const proxyData = new Proxy(data, {
get(target, key) {
let map = bucket.get(target);
if (!map) {
bucket.set(target, map = new Map())
}
let set = map.get(key);
if (!set) {
map.set(key, set = new Set())
}
set.add(activeEffect);
return target[key]
},
set(target, key, newVal) {
target[key] = newVal;
let map = bucket.get(target);
if (!map) return;
let set = map.get(key);
set.forEach(fn => fn())
}
})
effect(() => {
console.log(proxyData.ok ? proxyData.text : 321321321)
})
proxyData.ok = false
proxyData.text = 20
proxyData.text = 22
proxyData.text = 23
依旧存在问题: proxyData.ok = false后,函数中便不再使用到proxyData.text,但是proxyData.text改变时,函数依旧会执行。马上解决它,搞起!!!
进阶版effect
- effectFn函数添加一个dep属性值为数组,同时去收集执行时使用到的所有key的set。
设计图为:
- 每次effectFn执行前,会遍历所有收集到的set,然后把自己删除。此时,key被修改时便不会执行effectFn。
- 但每次删除之后函数会再次执行,此时用到的key将会再次将函数收集。此时不在用到text,之后text被修改,也就不在触发函数。
let activeEffect;
function effect(fn) {
const effectFn = () => {
cleaup(effectFn)
activeEffect = effectFn;
fn()
}
effectFn.dep = [];
effectFn()
}
function cleaup(effectFn) {
for (let i = 0; i < effectFn.dep.length; i++) {
const dep = effectFn.dep[i];
dep.delete(effectFn)
}
effectFn.dep.length = 0
}
const data = {
ok: true,
text: '哈哈哈'
}
const bucket = new WeakMap()
const proxyData = new Proxy(data, {
get(target, key) {
let map = bucket.get(target);
if (!map) {
bucket.set(target, map = new Map())
}
let set = map.get(key);
if (!set) {
map.set(key, set = new Set())
}
set.add(activeEffect);
activeEffect.dep.push(set);
return target[key]
},
set(target, key, newVal) {
target[key] = newVal;
let map = bucket.get(target);
if (!map) return;
let set = map.get(key);
set.forEach(fn => fn())
}
})
存在的问题: 运行之后会发现,无限循环。因为被修改时,遍历set执行了里面所有收集的函数。而执行函数时又会在set中添加函数。就如同:
const set = new Set([1]);
set.forEach(() => {
set.delete(1)
set.add(1)
console.log('遍历中')
})
完整版effect
只需要重新声明一个set就可以解决无限循环的问题:
const set = new Set([1]);
const newSet = new Set(set)
newSet.forEach(() => {
set.delete(1)
set.add(1)
console.log('遍历中')
})
完整代码
let activeEffect;
function effect(fn) {
const effectFn = () => {
cleaup(effectFn)
activeEffect = effectFn;
fn()
}
effectFn.dep = [];
effectFn()
}
function cleaup(effectFn) {
for (let i = 0; i < effectFn.dep.length; i++) {
const dep = effectFn.dep[i];
dep.delete(effectFn)
}
effectFn.dep.length = 0
}
const data = {
ok: true,
text: '哈哈哈'
}
const bucket = new WeakMap()
const proxyData = new Proxy(data, {
get(target, key) {
let map = bucket.get(target);
if (!map) {
bucket.set(target, map = new Map())
}
let set = map.get(key);
if (!set) {
map.set(key, set = new Set())
}
set.add(activeEffect);
activeEffect.dep.push(set);
return target[key]
},
set(target, key, newVal) {
target[key] = newVal;
let map = bucket.get(target);
if (!map) return;
let set = map.get(key);
const effectsToRun = new Set(set);
effectsToRun.forEach(fn => fn())
}
})
effect(() => {
console.log(proxyData.ok === true ? proxyData.text : 321321321)
})
console.log(activeEffect)
proxyData.ok = false
proxyData.text = 20
proxyData.text = 22
proxyData.text = 23