分支切换与cleanup

文章讲述了在JavaScript中使用Proxy实现的分支切换可能导致副作用函数遗留问题,通过在effect和trigger函数中添加清理逻辑来解决。关键点在于处理依赖收集和Set遍历的潜在无限循环问题。
摘要由CSDN通过智能技术生成

分支切换与cleanup

分支切换

在effect函数中使用三元表达式:

const data = { ok: true, text: 'hello world' }
const obj = new Proxy(data, { /* ... */ })

effect(function effectFn() {
  document.body.innerText = obj.ok ? obj.text : 'not'
})

其中effectFn中的三元表达式,会根据obj.ok的值的变化而切换到不同的代码分支。即当obj.ok的值发生变化时,会执行不同代码分支,这就是所谓的分支切换。

分支切换存在的问题

分支切换可能会存在遗留的副作用函数:
以上边代码为例,若obj.ok初始值为true,会读取obj.text的值,即此时会触发ok和text这两个字段的读操作,那么此时effectFn会与这两个字段产生联系:

data
    └── ok
        └── effectFn
    └── text
        └── effectFn

此时,副作用函数被ok和text这两个字段段依赖收集。但是当我把ok的值设为false,理想情况下:因为text不会被读取,那么此时是副作用只依赖于ok这一个字段。但是实际上,副作用函数仍然依赖于ok和text两个值,那么此时遗留的副作用函数会触发不必要的更新。

解决思路

解决这个问题的思路很简单,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。当副作用函数执行完毕后,会重新建立联系,但在新的联系中不会包含遗留的副作用函数,那么问题就迎刃而解。

如何解决?

要将一个副作用函数从所有与之关联的依赖集合中移除,就需要明确知道哪些依赖集合中包含它,那么我们可以在effect内部定义新的effectFn,并为effectFn添加deps属性,这是一个数组,用于存储所有的包含当前函数的依赖集合。

// 用一个全局变量存储被注册的副作用函数
let activeEffect
function effect(fn) {
  const effectFn = () => {
    // 当 effectFn 执行时,将其设置为当前激活的副作用函数
    activeEffect = effectFn
    fn()
  }
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

那么这个effectFn.deps中的元素是怎么收集的呢?其实是在track函数中进行收集

function track(target, key) {
  // 没有 activeEffect,直接 return
  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 中
  deps.add(activeEffect)
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps) // 新增
}

在为代理对象收集当前活跃的副作用函数的同时,副作用函数也将当前依赖这个函数的属性的依赖集合收集起来,那么此时在副作用函数端也可以看到收集了自己的依赖集合,即新增的代码是建立了这个关系:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

建立这个联系之后,每次副作用函数执行的时候,从effectFn.deps获取所有收集了自己的依赖集合,并将自身从这些集合中清除。

那么我们在cleanup函数执行上述的清除操作:

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.deps 数组
  effectFn.deps.length = 0
}

至此,副作用函数遗留的问题解决。但是在运行代码的时候,会发现进入了无限循环,问题出现在trigger中:

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn()) // 问题出在这句代码
}

在trigger中,我们遍历effect集合,而集合中存储着副作用函数。而**我们在执行副作用函数时,会调用cleanup清除依赖,但是副作用函数执行的时候又触发了读操作,又把副作用函数收集进依赖集合中。**此时等效于下面代码:

const set = new Set([1])

set.forEach(item => {
  set.delete(1)
  set.add(1)
  console.log('遍历中')
})

这段代码中,这个循环会一直执行下去。

set有关的语言规范

其实上述循环涉及到:在调用 forEach 遍历 Set 集合时,如果一个值已经被访问过了,但该值被删除并重新添加到集合,如果此时 forEach 遍历没有结束,那么该值会重新被访问,因此上述循环会无限执行。

解决方法

对这种情况,我们拷贝一个set进行遍历即可:

const set = new Set([1])

const newSet = new Set(set)
newSet.forEach(item => {
  set.delete(1)
  set.add(1)
  console.log('遍历中')
})

那么在trigger函数中,我们可以进行如下修改:

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

  const effectsToRun = new Set(effects)  // 新增
  effectsToRun.forEach(effectFn => effectFn())  // 新增
  // effects && effects.forEach(effectFn => effectFn()) // 删除
}

相同的,我们拷贝effects为effectsToRun进行遍历,即可避免无限执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值