如何避免无限递归?
为什么需要嵌套的副作用函数?
两个副作用函数之间会产生哪些影响?
4.1 响应式数据与副作用函数
什么是副作用函数
副作用函数指的是会产生副作用的函数
function effect() { document.body.innerText = "hello vue3"; } let val = 1; function effect() { val = 2; }
effect 函数的执行会直接或间接影响其他函数的执行,这时我们说 effect 函数产生了副作用。副作用很容易产生,例如一个函数修改了全局变量,这其实也是一个副作用。
什么是响应式数据
当值变化后,副作用函数自动重新执行,如果能实现这个目标,那么对象 obj 就是响应式数据。
4.2 响应式数据的基本实现
如何才能让 obj 变成响应式数据呢?
拦截一个对象的读取和设置操作
- 当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作;
- 当修改 obj.text 的值时,会触发字段 obj.text 的设置操作。
当读取字段 obj.text 时,我们可以把副作用函数 effect 存储到一个“桶”里。
当设置 obj.text 时,再把副作用函数 effect 从“桶”里取出并执行即可。
在 ES2015 之前,只能通过 Object.defineProperty 函数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使用代理对象 Proxy 来实现,这也是 Vue.js 3 所采用的方式。
简单实现
<body></body> <script> // 存储副作用函数的桶 const bucket = new Set() // 原始数据 const data = { text: 'hello world' } // 对原始数据的代理 const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { // 将副作用函数 effect 添加到存储副作用函数的桶中 bucket.add(effect) // 返回属性值 return target[key] }, // 拦截设置操作 set(target, key, newVal) { // 设置属性值 target[key] = newVal // 把副作用函数从桶里取出并执行 bucket.forEach(fn => fn()) } }) function effect() { document.body.innerText = obj.text } effect() </script>
4.3 设计一个完善的响应系统
一个响应系统的工作流程如下:
- 当读取操作发生时,将副作用函数收集到“桶”中;
- 当设置操作发生时,从“桶”中取出副作用函数并执行。
注册副作用函数的机制
为了副作用函数是一个匿名函数,也能够被正确地收集到 “桶”中。
<body></body> <script> // 存储副作用函数的桶 const bucket = new Set() // 原始数据 const data = { text: 'hello world' } // 对原始数据的代理 const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { // 将副作用函数 activeEffect 添加到存储副作用函数的桶中 bucket.add(activeEffect) // 返回属性值 return target[key] }, // 拦截设置操作 set(target, key, newVal) { // 设置属性值 target[key] = newVal // 把副作用函数从桶里取出并执行 bucket.forEach(fn => fn()) } }) // 用一个全局变量存储当前激活的 effect 函数 let activeEffect function effect(fn) { // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect activeEffect = fn // 执行副作用函数 fn() } effect(() => { console.log('effect run') document.body.innerText = obj.text // 匿名副作用函数与字段 obj.text 之间会建立响应联系 }) setTimeout(() => { obj.text2 = 'hello vue3' // 匿名副作用函数与字段 obj.text2 之间也会建立响应联系(这是一个问题) }, 1000) </script>
重新设计“桶”的数据结构
问题:
当读取属性时,无论读取的是哪一个属性,其实都一样,都会把副作用函数收集到“桶”里;当设置属性时,无论设置的是哪一个属性,也都会把“桶”里的副作用函数取出并执行。副作用函数与被操作的字段之间没有明确的联系。
使用一个 Set 数据结构作为存储副作用函数的“桶”。导致该问题(无差别建立响应问题)的根本原因是,我们没有在副作用函数与被操作的目标字段之间建立明确的联系。
target
和key
和effectFn
树形结构。
<body></body> <script> // 存储副作用函数的桶 const bucket = new WeakMap() // 原始数据 const data = { text: 'hello world' } // 对原始数据的代理 const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { // 将副作用函数 activeEffect 添加到存储副作用函数的桶中 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) // 返回属性值 return target[key] }, // 拦截设置操作 set(target, key, newVal) { // 设置属性值 target[key] = newVal // 把副作用函数从桶里取出并执行 const depsMap = bucket.get(target) if (!depsMap) return const effects = depsMap.get(key) effects && effects.forEach(fn => fn()) } }) // 用一个全局变量存储当前激活的 effect 函数 let activeEffect function effect(fn) { // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect activeEffect = fn // 执行副作用函数 fn() } effect(() => { console.log('effect run') document.body.innerText = obj.text }) setTimeout(() => { obj.text = 'hello vue3' }, 1000) </script>
WeakMap 和 Map 的区别
WeakMap 对 key 是弱引用,不影响垃圾回收器的工 作。据这个特性可知,一旦 key 被垃圾回收器回收,那么对应的键和 值就访问不到了。所以 WeakMap 经常用于存储那些只有当 key 所引 用的对象存在时(没有被回收)才有价值的信息,例如上面的场景中,如果 target 对象没有任何引用了,说明用户侧不再需要它了,这时垃圾回收器会完成回收任务。但如果使用 Map 来代替 WeakMap,那么即使用户侧的代码对 target 没有任何引用,这个 target 也不会被回收,最终可能导致内存溢出。
将桶逻辑封装
<body></body> <script> // 存储副作用函数的桶 const bucket = new WeakMap() // 原始数据 const data = { text: 'hello world' } // 对原始数据的代理 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) } }) function track(target, key) { 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) } function trigger(target, key) { const depsMap = bucket.get(target) if (!depsMap) return const effects = depsMap.get(key) effects && effects.forEach(fn => fn()) } // 用一个全局变量存储当前激活的 effect 函数 let activeEffect function effect(fn) { // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect activeEffect = fn // 执行副作用函数 fn() } effect(() => { console.log('effect run') document.body.innerText = obj.text }) setTimeout(() => { trigger(data, 'text') }, 1000) </script>
4.4 分支切换与 cleanup
副作用函数遗留情况,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。
当副作用函数执行完毕后,会重新建立联系,但在新的联系中不会包含遗留的副作用函数。
要将一个副作用函数从所有与之关联的依赖集合中移除,就需要明确知道哪些依赖集合中包含它,因此我们需要重新设计副作用函数。
const data = { text: 'helloWorld', ok: true } let activeEffect const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap function effect(fn) { const effectFn = () => { // 副作用函数执行之前,将该函数从其所在的依赖集合中删除 cleanup(effectFn) // 当effectFn执行时,将其设置为当前激活的副作用函数 activeEffect = effectFn fn() } effectFn.deps = [] // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合 effectFn() } function cleanup(effectFn) { for (let i = 0, len = effectFn.deps.length; i < len; i++) { let deps = effectFn.deps[i] // 依赖集合 deps.delete(effectFn) } effectFn.deps.length = 0 // 重置effectFn的deps数组 } const obj = new Proxy(data, { get(target, p, receiver) { track(target, p) return target[p] }, set(target, p, value, receiver): boolean { target[p] = value trigger(target, p) // 把副作用函数取出并执行 return true } }) // track函数 function track(target, key: string | symbol) { if (!activeEffect) return // 没有正在执行的副作用函数 直接返回 let depsMap = bucket.get(target) if (!depsMap) { // 不存在,则创建一个Map bucket.set(target, depsMap = new Map()) } let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数 if (!deps) { // 不存在,则创建一个Set depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) // 将副作用函数加进去 // deps就是当前副作用函数存在联系的依赖集合 // 将其添加到activeEffect.deps数组中 activeEffect.deps.push(deps) } // trigger函数 function trigger(target, key: string | symbol) { const depsMap = bucket.get(target) // target Map if (!depsMap) return; const effects = depsMap.get(key) // effectFn Set const effectToRun = new Set(effects) effectToRun && effectToRun.forEach(fn => { if (typeof fn === 'function') fn() }) } effect(() => { console.log('effect run') document.body.innerHTML = obj.ok ? obj.text : 'no' }) setTimeout(() => { obj.ok = false }, 1000) setTimeout(() => { obj.text = 'ds' }, 2000)
在调用 forEach 遍历 Set 集合 时,如果一个值已经被访问过了,但该值被删除并重新添加到集合, 如果此时 forEach 遍历没有结束,那么该值会重新被访问
<body></body> <script> const set = new Set([1]) const newSet = new Set(set) newSet.forEach(item => { set.delete(1) set.add(1) console.log(999) }) </script>
4.5 嵌套的 effect 与 effect 栈
用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。这时如果再
有响应式数据进行依赖收集,即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数。
使用副作用栈
const data = { foo: true, bar: true } let activeEffect, effectStack = [] const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap function effect(fn) { const effectFn = () => { // 副作用函数执行之前,将该函数从其所在的依赖集合中删除 cleanup(effectFn) // 当effectFn执行时,将其设置为当前激活的副作用函数 activeEffect = effectFn effectStack.push(activeEffect) // 将当前副作用函数推进栈 fn() // 当前副作用函数结束后,将此函数推出栈顶,并将activeEffect指向栈顶的副作用函数 // 这样:响应式数据就只会收集直接读取其值的副作用函数作为依赖 effectStack.pop() activeEffect = effectStack[effectStack.length - 1] } effectFn.deps = [] // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合 effectFn() } function cleanup(effectFn) { for (let i = 0, len = effectFn.deps.length; i < len; i++) { let deps = effectFn.deps[i] // 依赖集合 deps.delete(effectFn) } effectFn.deps.length = 0 // 重置effectFn的deps数组 } const obj = new Proxy(data, { get(target, p, receiver) { track(target, p) return target[p] }, set(target, p, value, receiver) { target[p] = value trigger(target, p) // 把副作用函数取出并执行 return true } }) // track函数 function track(target, key) { if (!activeEffect) return // 没有正在执行的副作用函数 直接返回 let depsMap = bucket.get(target) if (!depsMap) { // 不存在,则创建一个Map bucket.set(target, depsMap = new Map()) } let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数 if (!deps) { // 不存在,则创建一个Set depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) // 将副作用函数加进去 // deps就是当前副作用函数存在联系的依赖集合 // 将其添加到activeEffect.deps数组中 activeEffect.deps.push(deps) } // trigger函数 function trigger(target, key) { const depsMap = bucket.get(target) // target Map if (!depsMap) return; const effects = depsMap.get(key) // effectFn Set const effectToRun = new Set(effects) effectToRun && effectToRun.forEach(fn => { if (typeof fn === 'function') fn() }) } let tmp1, tmp2 effect(() => { console.log('eff1') effect(() => { console.log('eff2') tmp1 = obj.bar }) tmp2 = obj.foo }) // setTimeout(() => { // obj.foo = false // }, 1000) // setTimeout(() => { // obj.foo = false // }, 2000) obj.foo = false obj.foo = false obj.bar = false // 此处会执行3次! Vue的bug
响应系统的作用与实现(下)https://developer.aliyun.com/article/1392227