前言
上一篇文章的末尾我们提出来一个问题(传送门),今天我们来解答一下。
分支切换与cleanup
分支切换
首先我们来了解一下分支切换的概念,如下代码:
const data = { ok: true, text: 'Hello Vue3' }
const obj = new Proxy(data, {/*...*/})
effect(function effectFn() {
document.body.innerText = obj.ok ? obj.text : 'No'
})
在effect 函数中,存在一个三目运算符,随着obj.ok 的不同会执行不同的代码分支。当代码obj.ok 发生变化的时候,代码执行的分支会跟着变化,这就是所谓的分支切换。
分支切换会产生遗留的副作用函数
拿上面的obj.ok 来说,初始obj.ok = true 时,副作用函数effectFn 会触发obj.ok 和obj.text 两个属性的读取操作,此时副作用函数与响应式数据之间建立的联系如下:
data
└── ok
└── effectFn
└── text
└── effectFn
这时,无论修改ok 还是text 属性,都会触发副作用函数effectFn 的执行,到这一步是没有什么问题的。
我们来看看当obj.ok = false 时会发生什么
当obj.ok = false 时effectFn 函数中的代码便会走另一条分支,并不会读取obj.text 的值,也就是说不该触发obj.text 属性的读取操作,也不该建立该属性与副作用函数之间的联系。可是初始obj.ok = true 的时候已经建立起text 属性与副作用函数的联系了,这就导致修改text 属性的时候依旧会执行副作用函数。所以我们应该在一个适当的时机,将遗留的副作用函数清除。
将副作用函数从所有与之关联的依赖集合中移除
我们知道,每次对代理对象中的属性进行设置(赋值)操作的时候,都会触发与其相关联的副作用函数的执行,每次执行的时候又会触发读取操作。解决这个问题的思路很简单,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。代码如下:
function effect(fn) {
const effectFn = () => {
cleanup(effectFn) // 新增
// effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn
fn()
}
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [] // 新增
// 执行副作用函数
effectFn()
}
function track(target, key) {
if (!activeEffect) return
// 根据target 从‘桶’里取出一个depsMap, 它是一个Map 类型: key --> effects
let depsMap = bucket.get(target)
// 如果depsMap 不存在,则新建一个depsMap 加入桶中
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 根据key,从depsMap 中取出一个deps,它是一个Set 类型:effects
let deps = depsMap.get(key)
// 如果deps 不存在,则新建一个deps 加入depsMap 中
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 最后,将activeEffect 副作用函数加入‘桶’中
deps.add(activeEffect)
activeEffect.deps.push(deps) // 新增
}
function triger(target, key) {
let depsMap = bucket.get(target)
if (!depsMap) return
let deps = depsMap.get(key)
if (!deps) return
const effects = new Set(deps)
effects && effects.forEach((effect) => effect()) // 新增
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.activeDeps = []
}
我们新增了effectFn.deps 来存储所有与该副作用函数相关联得依赖集合,并在track 函数中将所有依赖集合收集到activeEffect.deps 中,即进行读取操作时收集依赖集合。
在开始执行副作用函数前调用cleanup 函数,清除所有集合中收集的当前effectFn。
除此之外还需要创建新的Set 来执行副作用函数,使用原Set 将会造成forEach 陷入死循环。
下面是完整代码:
let activeEffect
const bucket = new WeakMap()
const data = { text: 'Hello Vue3', ok: true }
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
},
})
function effect(fn) {
function effectFn() {
cleanup(effectFn)
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = bucket.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
function trigger(target, key) {
let depsMap = bucket.get(target)
if (!depsMap) return
let deps = depsMap.get(key)
if (!deps) return
const effects = new Set(deps)
effects && effects.forEach((effect) => effect())
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.activeDeps = []
}