本文已参与「新人创作礼」活动,一起开启掘金创作之路。
响应式原理-嵌套effect
上一篇中,我们实现了 vue响应式原理-分支切换的功能
1.业务场景
我们在平时的业务开发过程中,不可避免的会使用到嵌套组件,比如:
//A组件
const A={
render(){
...
}
}
//B组件
const B={
render(){
return <A/>
}
}
此时,effect就发生了嵌套,如下:
effect(()=>{
//关于B组件数据的读取
effect(()=>{
//关于B组件数据的读取
...
})
..
})
上面这样写,似乎不太清晰,我们可以用一个例子来看一下
2.例子
let obj={
flag:true,
count:1,
name:"zhangsan"
}
let activeEffect=null
let effect=(fn)=>{
//清空effectFn 的deps
const effectFn=()=>{
cleanUp(effectFn)
activeEffect=effectFn
fn()
}
effectFn.deps=[]
effectFn()
}
let bucket=new WeakMap()
let newObj=new Proxy(obj,{
get(target,key){
track(target,key)
return Reflect.get(target,key)
},
set(target,key,value){
Reflect.set(target,key,value)
trigger(target,key)
return true
},
})
//分支切换
function cleanUp(effectFn){
for (let i = 0; i < effectFn.deps.length; i++) {
const deps=effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length=0
}
function track(target,key){
let depMaps=bucket.get(target)
if(!depMaps){
bucket.set(target,(depMaps=new Map()))
}
let deps=depMaps.get(key)
if(!deps){
depMaps.set(key,(deps=new Set()))
}
deps.add(activeEffect)
// console.log(key);
activeEffect.deps.push(deps)
}
function trigger(target,key){
let depMaps=bucket.get(target)
let deps=depMaps.get(key)
let effectTorun=new Set(deps)
effectTorun.forEach(fn => {
fn()
});
}
//这里做了嵌套
effect(()=>{
effect(()=>{
document.getElementById("test").textContent=newObj.count
console.log("执行第二个函数");
})
console.log("执行第一个函数");
document.getElementById("app").textContent=newObj.name
})
如上:我们对effect做了一层嵌套,这里在执行effect函数的时候,会收集count和name属性的依赖 我们可以打印一下,bucket
我们的页面上,id为app的容器显示的文本内容是zhangsan,id为test的容器显示的文本内容是1,
如图
3.问题
接下来,我们去修改name的值,去触发依赖
...
newObj.name="lisi"
我们接着去看页面
咦,奇怪,为什么打印了执行第二个函数,我明明触发的name所收集的依赖,而且就算执行了里面嵌套effect,那我的在下面的赋值的语句为什么没有执行?
那如果我们修改count的值呢?结果显示没有问题,页面也更新为100,同时也执行了内部嵌套的effect
...
//newObj.name="lisi"
newObj.count=100
为什么修改name的值,会出现这个问题呢
4.分析
首先,收集依赖的时候是正常收集的
当我们修改name的值,去触发依赖,我们会执行这段代码,这段代码里的activeEffect 会被track收集,所以本质上,触发的依赖函数就是activeEffect
let activeEffect=null
let effect=(fn)=>{
//清空effectFn 的deps
const effectFn=()=>{
cleanUp(effectFn)
activeEffect=effectFn
fn()//执行fn
}
effectFn.deps=[]
effectFn()
}
fn是什么? 就是
effect(
//fn
()=>{
effect(()=>{
document.getElementById("test").textContent=newObj.count
console.log("执行第二个函数");
})
console.log("执行第一个函数");
document.getElementById("app").textContent=newObj.name
}
)
我们执行的时候,发现里面还有一个effect,于是我们将内部的effect,再次赋值给activeEffect,所以当我们去修改name的值的时候,activeEffect执行的时候,只会再次读取count的值,根本不能更新name的值!
5.解决方案
我们可以跟着上面的逻辑走,当我们执行到内部嵌套的函数的时候,我们如何才能再去往下执行呢?
正常的逻辑是:我们修改了name的值,会触发count收集的依赖,然后我们再去触发name的依赖。
我们可以在effect函数内部做一些“手脚” 我们在外面定义一个effectStack=[ ],当我们修改name的时候,effectStack[effect1()] (ps:此时是外层的依赖),然后执行到内部的effect,我们再将effect push进去 effectStack[ effect1() , effect2()]
当我们触发依赖的时候,先触发栈顶的effect,执行完成之后,删除掉它,同时把栈顶的函数,再次赋值给activeEffect
具体实现逻辑如下
let effectStack=[]//effect栈
let effect=(fn)=>{
//清空effectFn 的deps
const effectFn=()=>{
cleanUp(effectFn)
activeEffect=effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()//执行完成之后,删除栈顶函数
activeEffect=effectStack[effectStack.length-1] //将栈顶函数再次赋值
}
effectFn.deps=[]
effectFn()
}
再次修改newObj.name="lisi",页面也就可以看到效果了