响应式数据的基本实现
<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,那么这段代码就不能正确地工作了。
而我们希望的是,哪怕副作用函数是一个匿名函数,也能够被正确地收集到“桶”中。
为了实现这一点,我们需要提供一个用来注册副作用函数的机制
<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
<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
首先,我们需要明确分支切换的定义,如下面的代码所示:
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 属性,该属性是一个数组,用来存储所有包含当前副作用函数的依赖集合:
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 函数中:
function track(target, key) {
...
// 最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect)
activeEffect.deps.push(deps) // 新增
}
function track(target, key) {
...
// 最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect)
activeEffect.deps.push(deps) // 新增
}
有了这个联系后,我们就可以在每次副作用函数执行时,根据effectFn.deps 获取所有相关联的依赖集合,进而将副作用函数从依赖集合中移除:
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()
}
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 数组。
<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
// 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 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
<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>