Skip to content

响应式数据的基本实现

结果演示
vue
<template>
  <fieldset>
    <legend>结果演示</legend>
    <div id="demo"></div>
  </fieldset>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  const bucket = new Set()
  const data = { text: 'hello world' }

  const obj = new Proxy(data, {
    get(target, key) {
      bucket.add(effect)
      return target[key]
    },
    set(target, key, newVal) {
      target[key] = newVal
      bucket.forEach(fn => fn())
      return true
    }
  })

  function effect() {
    document.querySelector('#demo').innerText = obj.text
  }

  effect() // 触发读取
  // 修改响应式数据
  setTimeout(() => {
    obj.text = 'hello vue3'
  }, 1000)

})
</script>
<template>
  <fieldset>
    <legend>结果演示</legend>
    <div id="demo"></div>
  </fieldset>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  const bucket = new Set()
  const data = { text: 'hello world' }

  const obj = new Proxy(data, {
    get(target, key) {
      bucket.add(effect)
      return target[key]
    },
    set(target, key, newVal) {
      target[key] = newVal
      bucket.forEach(fn => fn())
      return true
    }
  })

  function effect() {
    document.querySelector('#demo').innerText = obj.text
  }

  effect() // 触发读取
  // 修改响应式数据
  setTimeout(() => {
    obj.text = 'hello vue3'
  }, 1000)

})
</script>

完善的响应系统-01

上一节的实现中,我们硬编码了副作用函数的名字(effect),导致一旦副作用函数的名字不叫 effect,那么这段代码就不能正确地工作了。
而我们希望的是,哪怕副作用函数是一个匿名函数,也能够被正确地收集到“桶”中。
为了实现这一点,我们需要提供一个用来注册副作用函数的机制

结果演示
vue
<template>
  <fieldset>
    <legend>结果演示</legend>
    <div id="demo2"></div>
  </fieldset>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  const bucket = new Set()
  const data = { text: 'hello world' }
  let activeEffect 

  const obj = new Proxy(data, {
    get(target, key) {
      if (activeEffect) { 
        bucket.add(activeEffect) 
      } 
      return target[key]
    },
    set(target, key, newVal) {
      target[key] = newVal
      bucket.forEach(fn => fn())
      return true
    }
  })

  function effect(fn) {
    activeEffect = fn
    fn()
  }

  effect(() => {
    // 输出两次
    console.log('effect run')
    document.querySelector('#demo2').innerText = obj.text
  }) // 触发读取
  // 修改响应式数据
  setTimeout(() => {
    // obj.text = 'hello vue3' 
    // 设置不存在的属性
    obj.notExist = 'hello vue3' 
  }, 1000)

})
</script>
<template>
  <fieldset>
    <legend>结果演示</legend>
    <div id="demo2"></div>
  </fieldset>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  const bucket = new Set()
  const data = { text: 'hello world' }
  let activeEffect 

  const obj = new Proxy(data, {
    get(target, key) {
      if (activeEffect) { 
        bucket.add(activeEffect) 
      } 
      return target[key]
    },
    set(target, key, newVal) {
      target[key] = newVal
      bucket.forEach(fn => fn())
      return true
    }
  })

  function effect(fn) {
    activeEffect = fn
    fn()
  }

  effect(() => {
    // 输出两次
    console.log('effect run')
    document.querySelector('#demo2').innerText = obj.text
  }) // 触发读取
  // 修改响应式数据
  setTimeout(() => {
    // obj.text = 'hello vue3' 
    // 设置不存在的属性
    obj.notExist = 'hello vue3' 
  }, 1000)

})
</script>

匿名副作用函数内部读取了字段 obj.text 的值,于是匿名副作用函数与字段 obj.text 之间会建立响应联系。
接着,我们开启了一个定时器,一秒钟后为对象 obj 添加新的 notExist 属性。
我们知道,在匿名副作用函数内并没有读取 obj.notExist 属性的值,所以理论上,字段 obj.notExist 并没有与副作用建立响应联系,
因此,定时器内语句的执行不应该触发匿名副作用函数重新执行。
但如果我们执行上述这段代码就会发现,定时器到时后,匿名副作用函数却重新执行了,这是不正确的。

导致该问题的根本原因是,我们没有在副作用函数与被操作的目标字段之间建立明确的联系。例如当读取属性时,无论读取的是哪一个属性,其实都一样,都会把副作用函数收集到“桶”里;当设置属性时,无论设置的是哪一个属性,也都会把“桶”里的副作用函数取出并执行。

完善的响应系统-02

为了解决以上问题,需要修改收集副作用函数的方式。

  • 用 target 来表示一个代理对象所代理的原始对象
  • 用 key来表示被操作的字段名
  • 用 effectFn 来表示被注册的副作用函数

那么可以为这三个角色建立如下关系:

target
  └─ key  
          └─ effectFn
target
  └─ key  
          └─ effectFn
结果演示
vue
<template>
  <fieldset>
    <legend>结果演示</legend>
    <div id="demo3"></div>
  </fieldset>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // 使用WeakMap 代替 Set 作为桶的数据结构
  // const bucket = new Set() 
  const bucket = new WeakMap() 
  const data = { text: 'hello world' }
  let activeEffect

  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
      track(target, key)
      // 返回属性值
      return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
      // 设置属性值
      target[key] = newVal
      // 把副作用函数从桶里取出并执行
      trigger(target, key)
      return true
    }
  })

  function track(target, key) {
    // 没有 activeEffect,直接 return
    if (!activeEffect) return
    // 根据 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)
  }

  function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap,它是 key --> effects
    const depsMap = bucket.get(target)
    if (!depsMap) return
    // 根据 key 取得所有副作用函数 effects
    const effects = depsMap.get(key)
    // 执行副作用函数
    effects && effects.forEach(fn => fn())
  }

  function effect(fn) {
    activeEffect = fn
    fn()
  }

  effect(() => {
    // 正常输出1次
    console.log('effect run3')
    document.querySelector('#demo3').innerText = obj.text
  }) // 触发读取
  // 修改响应式数据
  setTimeout(() => {
    // 设置不存在的属性
    obj.notExist = 'hello vue3'
  }, 1000)

})
</script>
<template>
  <fieldset>
    <legend>结果演示</legend>
    <div id="demo3"></div>
  </fieldset>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // 使用WeakMap 代替 Set 作为桶的数据结构
  // const bucket = new Set() 
  const bucket = new WeakMap() 
  const data = { text: 'hello world' }
  let activeEffect

  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
      track(target, key)
      // 返回属性值
      return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
      // 设置属性值
      target[key] = newVal
      // 把副作用函数从桶里取出并执行
      trigger(target, key)
      return true
    }
  })

  function track(target, key) {
    // 没有 activeEffect,直接 return
    if (!activeEffect) return
    // 根据 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)
  }

  function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap,它是 key --> effects
    const depsMap = bucket.get(target)
    if (!depsMap) return
    // 根据 key 取得所有副作用函数 effects
    const effects = depsMap.get(key)
    // 执行副作用函数
    effects && effects.forEach(fn => fn())
  }

  function effect(fn) {
    activeEffect = fn
    fn()
  }

  effect(() => {
    // 正常输出1次
    console.log('effect run3')
    document.querySelector('#demo3').innerText = obj.text
  }) // 触发读取
  // 修改响应式数据
  setTimeout(() => {
    // 设置不存在的属性
    obj.notExist = 'hello vue3'
  }, 1000)

})
</script>

从这段代码可以看出构建数据结构的方式,我们分别使用了WeakMap、Map 和 Set:

  • WeakMap 由 target --> Map 构成;
  • Map 由 key --> Set 构成。

其中 WeakMap 的键是原始对象 target,WeakMap 的值是一个Map 实例,
而 Map 的键是原始对象 target 的 key,Map 的值是一个由副作用函数组成的 Set。

分支切换与 cleanup

首先,我们需要明确分支切换的定义,如下面的代码所示:

javascript
const data = { ok: true, text: 'hello world' }
const obj = new Proxy(data, { /* ... */ })
effect(function effectFn() {
  document.body.innerText = obj.ok ? obj.text : 'not'
})
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 的值修改为 false,并触发副作用函数重新执行之后,这会触发更新,即副作用函数会重新执行。
但由于此时 obj.ok 的值为 false,所以不再会读取字段 obj.text 的值。
换句话说,无论字段 obj.text 的值如何改变,document.body.innerText 的值始终都是字符串 'not'。
所以最好的结果是,无论 obj.text 的值怎么变,都不需要重新执行副作用函数。

如果我们能做到每次副作用函数执行前,将其从相关联的依赖集合中移除,那么问题就迎刃而解了。
要将一个副作用函数从所有与之关联的依赖集合中移除,就需要明确知道哪些依赖集合中包含它,因此我们需要重新设计副作用函数,如下面的代码所示。
在 effect 内部我们定义了新的 effectFn函数,并为其添加了 effectFn.deps 属性,该属性是一个数组,用来存储所有包含当前副作用函数的依赖集合:

javascript
let activeEffect
function effect(fn){
  const effectFn = () => {
    activeEffect = effectFn
    fn()
  }
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []
  effectFn()
}
let activeEffect
function effect(fn){
  const effectFn = () => {
    activeEffect = effectFn
    fn()
  }
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []
  effectFn()
}

那么 effectFn.deps 数组中的依赖集合是如何收集的呢?其实是在 track 函数中:

javascript
  function track(target, key) {
    ...
    // 最后将当前激活的副作用函数添加到“桶”里
    deps.add(activeEffect)
    activeEffect.deps.push(deps) // 新增
  }
  function track(target, key) {
    ...
    // 最后将当前激活的副作用函数添加到“桶”里
    deps.add(activeEffect)
    activeEffect.deps.push(deps) // 新增
  }

有了这个联系后,我们就可以在每次副作用函数执行时,根据effectFn.deps 获取所有相关联的依赖集合,进而将副作用函数从依赖集合中移除:

javascript
let activeEffect
function effect(fn){
  const effectFn = () => {
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn)
    activeEffect = effect
    fn()
  }
  effectFn.deps = []
  effectFn()
}
let activeEffect
function effect(fn){
  const effectFn = () => {
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn)
    activeEffect = effect
    fn()
  }
  effectFn.deps = []
  effectFn()
}
javascript
function cleanup(effectFn){
  // 遍历 effectFn.deps 数组
  for(let i=0;i<effectFn.deps.length;i++){
    // deps 是依赖集合
    const deps = effectFn.dpes[i]
    // 将 effectFn 从依赖集合中移除
    deps.delete(effectFn)
  }
  // 最后需要重置 effectFn.deps 数组
  effectFn.length = 0
}
function cleanup(effectFn){
  // 遍历 effectFn.deps 数组
  for(let i=0;i<effectFn.deps.length;i++){
    // deps 是依赖集合
    const deps = effectFn.dpes[i]
    // 将 effectFn 从依赖集合中移除
    deps.delete(effectFn)
  }
  // 最后需要重置 effectFn.deps 数组
  effectFn.length = 0
}

cleanup 函数接收副作用函数作为参数,遍历副作用函数的effectFn.deps 数组,该数组的每一项都是一个依赖集合,然后将该副作用函数从依赖集合中移除,最后重置 effectFn.deps 数组。

结果演示
vue
<template>
  <fieldset>
    <legend>结果演示</legend>
    <div id="demo4"></div>
  </fieldset>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // 使用WeakMap 代替 Set 作为桶的数据结构
  const bucket = new WeakMap()
  const data = { text: 'hello world', ok: true }
  let activeEffect

  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
      track(target, key)
      // 返回属性值
      return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
      // 设置属性值
      target[key] = newVal
      // 把副作用函数从桶里取出并执行
      trigger(target, key)
      return true
    }
  })

  function track(target, key) {
    // 没有 activeEffect,直接 return
    if (!activeEffect) return
    // 根据 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)
    activeEffect.deps.push(deps)
  }

  function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap,它是 key --> effects
    const depsMap = bucket.get(target)
    if (!depsMap) return
    // 根据 key 取得所有副作用函数 effects
    const effects = depsMap.get(key)
    // 执行副作用函数
    // effects && effects.forEach(fn => fn()) 
    const effectsToRun = new Set(effects) 
    effectsToRun.forEach(fn => fn()) 
  }

  function effect(fn) {
    const effectFn = () => {
      cleanup(effectFn)
      activeEffect = effectFn
      fn()
    }
    effectFn.deps = []
    effectFn()
  }

  function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
      const deps = effectFn.deps[i]
      deps.delete(effectFn)
    }
    effectFn.deps.length = 0
  }

  effect(() => {
    console.log('effect run4')
    document.querySelector('#demo4').innerText = obj.ok ? obj.text : 'not'
  }) // 触发读取
  // 修改响应式数据
  setTimeout(() => {
    obj.ok = false
  }, 1000)
  setTimeout(() => {
  }, 1000)

})
</script>
<template>
  <fieldset>
    <legend>结果演示</legend>
    <div id="demo4"></div>
  </fieldset>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // 使用WeakMap 代替 Set 作为桶的数据结构
  const bucket = new WeakMap()
  const data = { text: 'hello world', ok: true }
  let activeEffect

  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
      track(target, key)
      // 返回属性值
      return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
      // 设置属性值
      target[key] = newVal
      // 把副作用函数从桶里取出并执行
      trigger(target, key)
      return true
    }
  })

  function track(target, key) {
    // 没有 activeEffect,直接 return
    if (!activeEffect) return
    // 根据 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)
    activeEffect.deps.push(deps)
  }

  function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap,它是 key --> effects
    const depsMap = bucket.get(target)
    if (!depsMap) return
    // 根据 key 取得所有副作用函数 effects
    const effects = depsMap.get(key)
    // 执行副作用函数
    // effects && effects.forEach(fn => fn()) 
    const effectsToRun = new Set(effects) 
    effectsToRun.forEach(fn => fn()) 
  }

  function effect(fn) {
    const effectFn = () => {
      cleanup(effectFn)
      activeEffect = effectFn
      fn()
    }
    effectFn.deps = []
    effectFn()
  }

  function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
      const deps = effectFn.deps[i]
      deps.delete(effectFn)
    }
    effectFn.deps.length = 0
  }

  effect(() => {
    console.log('effect run4')
    document.querySelector('#demo4').innerText = obj.ok ? obj.text : 'not'
  }) // 触发读取
  // 修改响应式数据
  setTimeout(() => {
    obj.ok = false
  }, 1000)
  setTimeout(() => {
  }, 1000)

})
</script>

嵌套的 effect

javascript
// effectFn1 嵌套了 effectFn2
effect(function effectFn1() {
  console.log('effectFn1 执行')
  effect(function effectFn2() {
    console.log('effectFn2 执行')
    // 在 effectFn2 中读取 obj.bar 属性
    temp2 = obj.bar
  })
  // 在 effectFn1 中读取 obj.foo 属性
  temp1 = obj.foo
})
// effectFn1 嵌套了 effectFn2
effect(function effectFn1() {
  console.log('effectFn1 执行')
  effect(function effectFn2() {
    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 执行'
'effectFn1 执行'
'effectFn2 执行'
'effectFn2 执行'

因为我们用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。这时如果再有响应式数据进行依赖收集,即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数,这就是问题所在。

为了解决这个问题,我们需要一个副作用函数栈 effectStack,在副作用函数执行时,将当前副作用函数压入栈中,待副作用函数执行完毕后将其从栈中弹出,并始终让 activeEffect 指向栈顶的副作用函数。这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现互相影响的情况。

避免无限递归循环

effect(() => {
  obj.foo = obj.foo + 1
})
effect(() => {
  obj.foo = obj.foo + 1
})

在这个语句中,既会读取 obj.foo 的值,又会设置 obj.foo 的值,这会导致无限递归循环。
我们可以尝试推理一下代码的执行流程:首先读取 obj.foo 的值,这会触发 track 操作,将当前副作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会触发 trigger 操作,即把“桶”中的副作用函数取出并执行。但问题是该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执行。这样会导致无限递归地调用自己,于是就产生了栈溢出。通过分析这个问题我们能够发现,读取和设置操作是在同一个副作用函数内进行的。此时无论是 track 时收集的副作用函数,还是 trigger 时要触发执行的副作用函数,都是activeEffect。基于此,我们可以在 trigger 动作发生时增加守卫条件:如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行

结果演示
vue
<template>
  <fieldset>
    <legend>结果演示</legend>
    <div id="demo4"></div>
  </fieldset>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // 使用WeakMap 代替 Set 作为桶的数据结构
  const bucket = new WeakMap()
  const data = { foo: 'foo', bar: 'bar', var: 1 }
  let activeEffect
  const effectStack = []  

  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
      track(target, key)
      // 返回属性值
      return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
      // 设置属性值
      target[key] = newVal
      // 把副作用函数从桶里取出并执行
      trigger(target, key)
      return true
    }
  })

  function track(target, key) {
    // 没有 activeEffect,直接 return
    if (!activeEffect) return
    // 根据 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)
    activeEffect.deps.push(deps)
  }

  function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap,它是 key --> effects
    const depsMap = bucket.get(target)
    if (!depsMap) return
    // 根据 key 取得所有副作用函数 effects
    const effects = depsMap.get(key)
    // 执行副作用函数 避免无限循环
    const effectsToRun = new Set()
    effects && effects.forEach(fn => {  
      if (fn !== activeEffect) {  
        effectsToRun.add(fn)  
      }  
    })  
    effectsToRun.forEach(fn => {
      // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
      if (fn.options.scheduler) { // 新增
        fn.options.scheduler(fn) // 新增
      } else {
        // 否则直接执行副作用函数(之前的默认行为)
        fn() // 新增
      }
    })
  }

  function effect(fn) {
    const effectFn = () => {
      cleanup(effectFn)
      activeEffect = effectFn
      // 在调用副作用函数之前将当前副作用函数压入栈中
      effectStack.push(activeEffect)  
      fn()
      // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
      effectStack.pop()  
      activeEffect = effectStack.at(-1)  
    }
    effectFn.deps = []
    effectFn()
  }

  function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
      const deps = effectFn.deps[i]
      deps.delete(effectFn)
    }
    effectFn.deps.length = 0
  }

  effect(function effectFn1() {
    console.log('effectFn1 执行')
    effect(function effectFn2() {
      console.log('effectFn2 执行')
      // 在 effectFn2 中读取 obj.bar 属性
      let temp2 = obj.bar
    })
    // 在 effectFn1 中读取 obj.foo 属性
    let temp1 = obj.foo
  })
  effect(() => {
    console.log('obj.var++')
    obj.var++
  })
  // 修改响应式数据
  setTimeout(() => {
    // obj.foo = 'asdf'
    obj.bar = 'asdf'
  }, 1000)

})
</script>
<template>
  <fieldset>
    <legend>结果演示</legend>
    <div id="demo4"></div>
  </fieldset>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // 使用WeakMap 代替 Set 作为桶的数据结构
  const bucket = new WeakMap()
  const data = { foo: 'foo', bar: 'bar', var: 1 }
  let activeEffect
  const effectStack = []  

  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
      track(target, key)
      // 返回属性值
      return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
      // 设置属性值
      target[key] = newVal
      // 把副作用函数从桶里取出并执行
      trigger(target, key)
      return true
    }
  })

  function track(target, key) {
    // 没有 activeEffect,直接 return
    if (!activeEffect) return
    // 根据 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)
    activeEffect.deps.push(deps)
  }

  function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap,它是 key --> effects
    const depsMap = bucket.get(target)
    if (!depsMap) return
    // 根据 key 取得所有副作用函数 effects
    const effects = depsMap.get(key)
    // 执行副作用函数 避免无限循环
    const effectsToRun = new Set()
    effects && effects.forEach(fn => {  
      if (fn !== activeEffect) {  
        effectsToRun.add(fn)  
      }  
    })  
    effectsToRun.forEach(fn => {
      // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
      if (fn.options.scheduler) { // 新增
        fn.options.scheduler(fn) // 新增
      } else {
        // 否则直接执行副作用函数(之前的默认行为)
        fn() // 新增
      }
    })
  }

  function effect(fn) {
    const effectFn = () => {
      cleanup(effectFn)
      activeEffect = effectFn
      // 在调用副作用函数之前将当前副作用函数压入栈中
      effectStack.push(activeEffect)  
      fn()
      // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
      effectStack.pop()  
      activeEffect = effectStack.at(-1)  
    }
    effectFn.deps = []
    effectFn()
  }

  function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
      const deps = effectFn.deps[i]
      deps.delete(effectFn)
    }
    effectFn.deps.length = 0
  }

  effect(function effectFn1() {
    console.log('effectFn1 执行')
    effect(function effectFn2() {
      console.log('effectFn2 执行')
      // 在 effectFn2 中读取 obj.bar 属性
      let temp2 = obj.bar
    })
    // 在 effectFn1 中读取 obj.foo 属性
    let temp1 = obj.foo
  })
  effect(() => {
    console.log('obj.var++')
    obj.var++
  })
  // 修改响应式数据
  setTimeout(() => {
    // obj.foo = 'asdf'
    obj.bar = 'asdf'
  }, 1000)

})
</script>