likes
comments
collection
share

你可曾听过vue3响应式的分支切换?用这个来惊艳面试官吧!(全面解析vue3响应式第三期)

作者站长头像
站长
· 阅读数 3

前言

本系列是个人深入学习vue3响应式的笔记,预期分三期发布。在这个系列中,我将深入浅出的,手把手带大家搓一个完善的响应式系统,同时科普响应式系统中的专业术语。

什么是分支切换?

分支切换需要解决的问题是:清除依赖集合中遗留的无用的副作用函数,避免性能浪费

什么是依赖集合?

某一字段对应的所有副作用函数集合就是依赖集合 下面是我做的,响应式系统的数据结构 你可曾听过vue3响应式的分支切换?用这个来惊艳面试官吧!(全面解析vue3响应式第三期)

什么是分支切换?

假设原始对象data中有两个字段

const data = {
	ok: true,
	text: "hello vue3"
}

然后在响应式系统中,原始对象data的代理对象为obj

const obj = new Proxy(data, {
	get(target, key){
		track(target, key)
		return target[key]
	},
	set(target, key, newVal){
		target[key] = newVal
		trigger(target, key)
		return true
	}
})

代理对象中的track函数和trigger函数在之前的文章中有讲到,这里简单介绍一下:

  • track(target, key):用来将当前注册的副作用函数添加到原始对象target 的特定字段key 对于的副作用函数集合中
  • tirgger(target, key):用来触发原始对象target 的特定字段key 对于的所有副作用函数

现在我们为代理对象obj添加副作用函数

effect(()=>{
	document.body.innerText = obj.ok ? obj.text : "我现在与obj.text无关了"
})

在我们这个副作用函数中,body标签的文本在最开始的时候依赖两个字段,obj.ok和obj.text。 因为obj.ok的值为true,所以body标签的文本值为obj.text; 而当我们修改obj.ok的值为false时

obj.ok = false

此时body标签的文本只依赖于一个字段,obj.ok 无论obj.text的值为什么,body标签的文本值等于一个字符串"我现在与obj.text无关了" ,由于body标签不再依赖于obj.text,所以我们希望与之相关的副作用也不再执行。

目前字段与副作用函数的依赖关系为: 你可曾听过vue3响应式的分支切换?用这个来惊艳面试官吧!(全面解析vue3响应式第三期)

你可曾听过vue3响应式的分支切换?用这个来惊艳面试官吧!(全面解析vue3响应式第三期)

每当我们修改字段text的值时,都会触发一遍Fn函数,而text字段已经与body标签的值没有依赖关系了。这就造成性能的浪费。

那么如何解决这个问题呢?答案是: 每次为特定字段的依赖集合添加副作用函数时,先将该副作用函数从所有的依赖集合中删除 只要每次向字段的依赖集合添加副作用函数的时候,所有的依赖集合里不包含该副作用函数,就能确保副作用函数的按需添加,就不会有遗留的无用的副作用函数

那么我们如何将特定副作用函数从所有的依赖集合中删除呢?目前我们的数据结构还做不到。因为副作用函数和依赖集合的关系是单向的,只能通过依赖集合找到里面的副作用函数,而不能根据副作用函数找得到相关的依赖集合。所以我们首先要重写副作用注册函数effect,为副作用函数添加一个数组,将与副作用函数有关的依赖集合都放到这个数组里。这样我们就建立了副作用函数与依赖集合的双向关系。

在重写副作用函数注册函数effect之前,让我们看看旧的effect是如何实现的

function effect(fn){
	//首先将全局遍历avtiveEffect的值设置为副作用函数fn
	activeEffect = fn
	//然后执行副作用函数fn,进行初始化
	fn()
}

其中的activeEffect是为了让我们的响应式系统不再依赖副作用的函数名,而创建的全局遍历,当前注册的副作用函数都会赋值给activeEffect。详细的可以看之前的文章,教你如何手写不依赖副作用函数名的vue3响应式系统。

然后我们为副作用函数注册函数添加数组,我们知道activeEffect指的是当前注册的副作用函数,我们在添加副作用函数到依赖集合中时,就是将activeEffect的函数添加到依赖集合中。所以添加数组,我们可以从activeEffect入手

function effect(fn){
	//创建一个新的函数
	function effectFn(fn){
		//将目前注册的副作用函数设置为这个新的函数
		activeEffect = effectFn
		//执行副作用函数,初始化
		fn()
	}
	//为新的函数添加数组,这个数组里存放与副作用函数有关的所以依赖集合
	effectFn.deps = []
	//然后执行effectFn函数,来注册副作用函数,执行副作用函数来初始化
	effectFn()
}

由于函数的本质是一个特殊的对象,所以可以向函数添加属性

那么用来存放副作用函数对应的 所有依赖集合的数组有了,我们如何向这个数组里添加相关的依赖集合呢?答案是重写track函数。

track函数用于:向原始对象target的特定字段key的依赖集合中添加当前注册的副作用函数

那么在track函数中,我们就可以获得当前副作用函数对应的依赖集合

我们还是先看看之前的track函数是如何写的

function track(target, key){
	//如果没有注册过副作用函数,直接返回
	if(!activeEffect) return
	//获取原始对象target对应的 所有字段对应的所有副作用函数映射Map
	let depsMap = bucket.get(target)
	//如果没有映射的话,创建一个
	if(!depsMap){
		depsMap = new Map()
		bucket.set(target, depsMap)
	}
	//然后通过字段key获得对应的所有副作用函数的集合Set
	let deps = depsMap.get(key)
	//如果没有集合的话,创建一个
	if(!deps){
		deps = new Set()
		depsMap.set(key, deps)
	}
	//将副作用函数添加到字段key对应的副作用函数的集合中
	deps.add(activeEffect)
}

然后在旧的track函数的基础上,只需要加一行代码,向当前注册的副作用函数activeEffect的deps数组中添加对应的副作用函数的集合就好

function track(target, key){
	//如果没有注册过副作用函数,直接返回
	if(!activeEffect) return
	//获取原始对象target对应的 所有字段对应的所有副作用函数映射Map
	let depsMap = bucket.get(target)
	//如果没有映射的话,创建一个
	if(!depsMap){
		depsMap = new Map()
		bucket.set(target, depsMap)
	}
	//然后通过字段key获得对应的所有副作用函数的集合Set
	let deps = depsMap.get(key)
	//如果没有集合的话,创建一个
	if(!deps){
		deps = new Set()
		depsMap.set(key, deps)
	}
	//将副作用函数添加到字段key对应的副作用函数的集合中
	deps.add(activeEffect)
	//向副作用函数的deps数组中,添加对应的依赖集合
	activeEffect.deps.push(deps)
}

然后根据重写的track函数,我们实现了从副作用函数到对应的依赖集合,从依赖集合到对应的副作用函数的 双向联系。

现在我们可以通过重写的effect函数和track函数实现了副作用函数和依赖集合的双向联系,现在我们写一个cleanup函数,用来清除当前注册的副作用函数与依赖集合的关系,即从所有相关的依赖集合中删除当前注册的副作用函数。通过遍历当前注册的副作用函数的deps数组就可以做到

function cleanup(effectFn){
	//遍历副作用函数的deps数组
	for(let i=0; i<effectFn.deps.length; i++){
		//从所有有关的依赖集合中删除 副作用函数
		effectFn.deps[i].delete(effectFn)
	}
	//最后重置副作用函数的deps数组
	deps.length = 0
}

然后只需在每次注册副作用函数的时候,先调用cleanup函数,切断当前注册的副作用函数与所有相关的依赖集合的联系,然后再调用一次副作用函数初始化就好了

function effect(fn){
	function effectFn(){
		avtiveEffect = effectFn
		//先切除副作用函数与依赖集合的联系
		cleanup(effectFn)]
		//然后再运行副作用函数,进行初始化
		fn()
	}
	effectFn.deps = []
	//先切除副作用函数与依赖集合的联系
	//然后再运行副作用函数,进行初始化
	effectFn()
}

但是我们的代码到目前还无法运行,为什么呢?因为trigger函数出现了无限循环。让我们先看看旧的trigger函数。 trigger函数用来触发原始对象target 的特定字段key 对于的所有副作用函数

function trigger(target, key){
	//先根据原始对象target获取 所有字段和所有副作用函数的映射Map
	let depsMap = bucket.get(target)
	//如果为空,直接返回
	if(!depsMap) return
	//然后根据字段key,获取对于的所有副作用函数
	let deps = depsMap.get(key)
	//利用JS条件判断的短路特性,如果deps有值的话,依次执行deps里面的副作用函数
	//如果deps为undefined的话,不执行后面的语句

	//无限循环的问题旧出在这个forEach上
	deps && deps.forEach(fn => fn())
}

为什么会出现无限循环呢?是因为在执行副作用函数时,读取了相关字段的值,触发了代理对象obj的get拦截,然后在get拦截中,又通过track函数向依赖集合把之前删除的副作用函数又添加了回来。根据ECMA2020对forEach的规范: 在调用forEach遍历元素时,如果某元素已经被遍历过,随后被删除,然后在添加进数组中,若forEach遍历还未结束,则会再次遍历该元素。

我们如何解决这个无限循环的问题呢?很简单,将读写分离就好。我们将副作用函数集合复制一份,用复制的一份来读,原来的一份写入

function trigger(target, key){
	let depsMap = bucket.get(taregt)
	if(!depsMap) return
	let effects = depsMap.get(key)

	let effectsToRun = new Set(effects)
	effectsToRun.forEach(fn => fn())
}

这样子我们就完成了响应式系统的分支切换,下面是完整代码

const data = {
	ok: true,
	text: "ok为真"
}

function cleanup(effectFn) {
	if(!effectFn.deps) return
	for (let i = 0; i < effectFn.deps.length; i++) {
		effectFn.deps[i].delete(effectFn)
	}
	effectFn.deps.length
}

let bucket = new WeakMap()

let activeEffect = undefined
function effect(fn) {
	function effectFn() {
		activeEffect = effectFn
		cleanup(effectFn)
		fn()
	}
	effectFn.deps = []
	effectFn()
}

function track(target, key) {
	if (!activeEffect) return
	let depsMap = bucket.get(target)
	if (!depsMap) {
		depsMap = new Map()
		bucket.set(target, depsMap)
	}
	let deps = depsMap.get(key)
	if (!deps) {
		deps = new Set()
		depsMap.set(key, deps)
	}
	deps.add(activeEffect)

	activeEffect.deps.push(deps)
}

function trigger(target, key) {
	let depsMap = bucket.get(target)
	if (!depsMap) return
	let effects = depsMap.get(key)

	let effectsToRun = new Set(effects)
	effectsToRun.forEach(fn => fn())
}

const obj = new Proxy(data, {
	get(target, key) {
		console.log(`触发读取 ${target}.${key}`)
		track(target, key)
		return target[key]
	},
	set(target, key, newVal) {
		console.log(`触发设置 ${target}.${key} = ${newVal}`)
		target[key] = newVal
		return true
	}
})

effect(() => {
	document.body.innerText = obj.ok ? obj.text : "现在body标签的值与obj.text无关了"
	obj.ok = false
	console.log("触发回调函数")
})

setTimeout(()=>{
	obj.text = "我不会触发回调函数"
}, 1000)