你可曾听过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,所以我们希望与之相关的副作用也不再执行。
目前字段与副作用函数的依赖关系为:
每当我们修改字段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)
转载自:https://juejin.cn/post/7352387617629470760