我们给effect函数加入lazy属性,来实现懒执行,在有些场景下,我们并不希望它立即执行,而是希望在需要的时候才执行,例如计算属性,我们在options里面添加lazy属性来达到目的,即如下:
effect(
()=>{console.log(obj.foo)},
{
lazy:true
}
)
lazy选项和之前介绍的scheduler一样,它通过options选项对象指定,有了它,我们就可以修改effect函数实现逻辑,当options.lazy为true时,则不立即执行副作用函数:
function effect(fn,options={}){
const effectFn=()=>{
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length-1]
return res
}
effectFn.deps=[]
effectFn.options = options
if(!options.lazy){
effectFn()
}
return effectFn
}
我们想实现通过定义computed函数来实现计算,实现方法如下:
function computed(getter){
let value
// dirty用来标识是否重新计算值
let dirty = true
const effectFn=effect(
getter,
{
lazy:true,
// 属性值发生变化需要dirty值变为true,重新计算
scheduler(){
dirty = true
}
}
)
const obj = {
get value(){
if(dirty){
value = effectFn()
// 将dirty设置为false,下一次访问直接使用缓存到value中的值
dirty = false
}
return value
}
}
return obj
}
使用时:
const sumRes = computed(()=>obj.bar+obj.foo)
console.log(sumRes.value)
但假如我们在外面这样使用:
const sumRes = computed(()=>obj.bar+obj.foo)
effect(()=>{
console.log(sumRes.value)
})
obj.foo++
console.log(sumRes.value)
那么effect函数里还是打印2
原因:
从本质上看这就是一个典型的 effect 嵌套。一个计算属性内部拥有自己的 effect,并且它是懒执行的,只有当真正读取计算属性的值时才会执行。对于计算属性的 getter 函数来说,它里面访问的响应式数据只会把computed 内部的 effect 收集为依赖。而当把计算属性用于另外一个 effect 时,就会发生 effect 嵌套,外层的 effect 不会被内层 effect 中的响应式数据收集。
解决办法:
我们可以手动调用 track 函数进行追踪;当计算属性依赖的响应式数据发生变化时,我们可以手动调用trigger 函数触发响应:
function computed(getter){
let value
let dirty = true
const effectFn=effect(
getter,
{
lazy:true,
scheduler(){
if(!dirty){
dirty = true
// 当计算属性依赖响应式数据变化时,手动调用trigger函数
trigger(obj,'value')
}
}
}
)
const obj = {
get value(){
if(dirty){
value = effectFn()
dirty = false
}
// 当读取value时,手动调用track函数追踪
track(obj,'value')
return value
}
}
return obj
}
最后整体代码如下:可自行测试
const data={foo:1,bar:1}
let activeEffect;
const bucket = new WeakMap()
const effectStack = []
function effect(fn,options={}){
const effectFn=()=>{
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length-1]
return res
}
effectFn.deps=[]
effectFn.options = options
if(!options.lazy){
effectFn()
}
return effectFn
}
function cleanup(effectFn){
for(let i=0;i<effectFn.deps.length;i++){
effectFn.deps[i].delete(effectFn)
}
effectFn.deps.length = 0
}
function track(target,key){
if(!activeEffect)return
let depsMap = bucket.get(target)
if(!depsMap){bucket.set(target,depsMap=new Map())}
let deps = depsMap.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
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn=>{
if(effectFn!==activeEffect){
effectsToRun.add(effectFn)
}
})
// 调度执行
effectsToRun.forEach(effectFn=>{
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn)
}else{
effectFn()
}
})
}
const obj = new Proxy(data,{
get(target,key){
track(target,key)
return target[key]
},
set(target,key,newVal){
target[key] = newVal
trigger(target,key)
}
})
function computed(getter){
let value
let dirty = true
const effectFn=effect(
getter,
{
lazy:true,
scheduler(){
if(!dirty){
dirty = true
// 当计算属性依赖响应式数据变化时,手动调用trigger函数
trigger(obj,'value')
}
}
}
)
const obj = {
get value(){
if(dirty){
value = effectFn()
dirty = false
}
// 当读取value时,手动调用track函数追踪
track(obj,'value')
return value
}
}
return obj
}
const sumRes = computed(()=>obj.bar+obj.foo)
effect(
()=>{
console.log(sumRes.value)
}
)
obj.foo++
console.log(sumRes.value)