vuejs 设计与实现(嵌套的effect与effect栈)

本文讲述了JavaScript中effect函数的嵌套使用导致副作用执行混乱的问题,通过创建effectStack和activeEffect变量,以及依赖管理机制来解决因嵌套引发的副作用函数执行顺序问题。
摘要由CSDN通过智能技术生成

effect 是可以发生嵌套的

effect(function effectFn1(){
    effect(function effectFn2(){
        /* .... */
    })
    /* .... */
})

如下代码嵌套执行

// 全局变量
let temp1, temp2

// effectFn1 嵌套了 effectFn2
effect(function effectFn1(){
  console.log('effectFn1 执行')

  effect(function effect2(){
    console.log('effectFn2 执行')
    // 在 effectFn2 中读取 obj.bar 属性
    temp2 = obj.bar
  })

   // 在 effectFn1 中读取 obj.foo 属性
    temp1 = obj.foo
})

问题:希望当修改obj.foo时会触发effectFn1执行。由于effectFn2嵌套在effectFn1里,所以会间接触发effectFn2执行,而当修改obj.bar时,只会触发俄effectFn2。但结果 不是这样,当修改obj.foo时,输出为:

'effectFn1 执行'
'effectFn2 执行'
'effectFn2 执行'

原因:发生嵌套时activeEffect会存储内层副作用函数,不会恢复到原来的值。

解决办法:创建一个effectStack栈,副作用函数执行时,将当前副作用函数压入栈中,待副作用函数执行完将其从栈中弹出,并始终让activeEffect指向栈顶的副作用函数:

// 用一个全局变量储存被注册的副作用函数
let activeEffect

// effect 栈
const effectStack = []

// effect 函数用于注册副作用函数
function effect(fn){
  // 当 effectFn 执行时,将其设置为当前激活的副作用函数
  const effectFn = ()=>{
    // 调用cleanup完成清除工作
    cleanup(effectFn)
    // 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
    activeEffect = effectFn
    // 在调用副作用函数前将其压入栈中
    effectStack.push(effectFn)
    // 执行副作用函数
    fn()
    // 在当前副作用函数执行完后,将其弹出,并把activeEffect还原为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  // activeEffect.deps 用来存储所有与被副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

// 清除工作
function cleanup(effectFn){
 // 遍历`effectFn.deps 数组
 for(let i=0; i< effectFn.deps.length; i++){
   // deps 是依赖集合
   const deps = effectFn.deps[i]

   // 将 effectFn 从依赖集合中移除
   deps.delete(effectFn)
 }
 // 最后需要重置 effectFn.deeps 数组
 effectFn.deps.length = 0
}

// 储存副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { text: 'hello world',ok:true, foo:true, bar:true,three:true}

// 对原始数据的代理
const obj = new Proxy(data,{
  // 拦截读取操作
  get(target,key){
    
    track(target,key)

    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target,key,newValue){
    console.log('bucket:',bucket)
    // 设置属性值
    target[key] = newValue
   
    trigger(target,key)

    return true
  }
})

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target,key){
  // 没有 activeEffect 直接 return
    if(!activeEffect) return target[key]
    // 根据 target 从 “桶” 中 取得 depsMap,它也是一个map 类型 key --》effects
    let depsMap = bucket.get(target)
    // 如果不存在 depsMap 那么新建一个Map 并与 target 关联
    if(!depsMap){
      bucket.set(target,(depsMap = new Map()))
    }
    // 再根据 key 从 depsMap 中取得 deps,它是一个 set 类型
    // 里面储存着所有与当前key 相关联的副作用函数,effects
    let deps = depsMap.get(key)
    // 如果 deps 不存在 同样新建一个set 并与 key 关联
    if(!deps){
      depsMap.set(key,(deps = new Set()))
    }
    // 最后将当前激活的副作用函数添加到“桶”
    deps.add(activeEffect)

    // deps 就是一个与当前副作用函数存在关联的依赖集合
    // 将其添加到activeEffect.deps 数组中
    activeEffect.deps.push(deps)  // 新增
}
// 在 set 拦截函数内调用 trigger 函数 触发变化
function trigger(target,key){
   // 根据 target 从桶中取得 depsMap,它是key --》effects
    const depsMap = bucket.get(target)
    if(!depsMap) return true
    // 根据 key 取得所有副作用函数 effects
    const effects = depsMap.get(key)

    // new Set 是为了防止 effectFn 执行时,进行无限的循环
    const effectsToRun = new Set()
    
    effects && effects.forEach(effectFn => {
      // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
      if(effectFn !== activeEffect){
        effectsToRun.add(effectFn)
      }
    })
    // 执行副作用函数
    effectsToRun && effectsToRun.forEach(fn => fn())
}

// 全局变量
let temp1, temp2, temp3

// effectFn1 嵌套了 effectFn2
effect(function effectFn1(){
  console.log('effectFn1 执行')

  effect(function effect2(){
    console.log('effectFn2 执行')
    // 在 effectFn2 中读取 obj.bar 属性
    temp2 = obj.bar

    effect(function effect3(){
      console.log('effectFn3 执行')

      // 在 effectFn3 中 读取 obj.three 属性
      temp3 = obj.three
    })
  })

   // 在 effectFn1 中读取 obj.foo 属性
    temp1 = obj.foo
})

setTimeout(()=>{
  obj.bar = false
},2000)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值