Vue3 设计响应式系统(三)——分支切换与cleanup

189 阅读3分钟

前言

上一篇文章的末尾我们提出来一个问题(传送门),今天我们来解答一下。

分支切换与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 = []
}