响应式系统实现
响应式基础
通过 proxy 代理对象,读取属性时触发 get 方法,设置属性时触发 set 方法
在 get 方法中收集副作用函数,在 set 方法中触发副作用函数
假设有这么一个对象:{ ok: true, text: 'hello world' },注册副作用函数
| effect(() => { document.body.innerHTML = obj.ok ? obj.text : 'ok not' })
|
当 ok 为 true,我们会走 obj.text 的读取逻辑,触发 get 拦截方法。这个时候页面上会显示 hello world,当我们将 obj.ok 设置为 false 后,我们会有副作用遗留函数(text的),因为 ok 为 false,永远不会再读取 obj.text 了。
但是,当我们修改 obj.text 时, effect 副作用函数依然会触发,虽然页面上永远是 ok not。
清除不必要的副作用函数
这时,我们就需要进行 分支切换和 cleanup 函数了,通过 cleanup 函数,我们将只收集使用到的 key 的副作用函数,也就是说,当 ok 为false 时,我们不再对 obj.text 进行依赖手机,无论我们如何修改 obj.text,都不会触发 effect 方法。
我们对注册副作用函数,做一些改变,在注册函数内部,定义了一个新的副作用函数方法,这个方法内部执行 删除副作用函数的方法 和真正的副作用函数(设置 document.body 内容)
同时,我们还在 副作用函数 上定义了一个 deps 属性,用来存储与该副作用函数相关联的依赖集合,将来在 cleanup 中通过 activeEffect.deps[i] delete effectFn 时,其实,就是将 某个 key 的 deps 中的副作用函数删除了
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function effect(fn) {
function effectFn() { activeEffect = effectFn
cleanup(effectFn)
fn() }
effectFn.deps = []
effectFn() }
|
- 定义 cleanup 函数,每一次触发 set 方法时,都会先删除所有 key 对应的依赖,然后重新执行
effectFn 内部的 fn 函数,重新收集依赖
因为 obj.ok = false 了,不会再读取 obj.text,所以也就不会再对 obj.text 收集依赖了。
1 2 3 4 5 6 7 8 9 10
| function cleanup(effectFn) {
for(let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i]
deps.delete(effectFn) }
effectFn.deps.length = [] }
|
注意
我们删除 effectFn.deps[i] 中的 effectFn,其实就是删除了 ‘ok、text’ 中对应的依赖集合(Set)的副作用函数,因为 effectFn.deps 中存放的集合 和 Map key 对应的 Value 的集合,是同一个集合
删除后,Map 中的所有 value 都是空的 Set
然后执行 fn 函数,重新进行读取 obj 属性,进行依赖收集。
竟然无限循环?
- 最后一步,我们还需要改造一下 trigger 函数,否则会造成
无限循环
1 2 3 4 5 6 7 8 9 10 11
| function trigger(target, key) { const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key) const effectsToRun = new Set(effects)
effectsToRun && effectsToRun.forEach(fn => fn()) }
|
为什么新建一个 Set 集合呢? const effectsToRun = new Set(effects)
因为,我们遍历 effects 时,执行了每一个 副作用 函数,当副作用函数执行时,会调用 cleanup 进行清除,实际上就是从 effects 集合中奖当前执行的副作用函数剔除
但是,副作用函数的执行 fn() 会触发属性的读取操作,执行 track,导致副作用函数重新被收集到依赖中,而对于 effects集合的遍历仍然在执行,从而造成 无限循环。
解决办法就是:根据 effects 重新建立一个集合,进行遍历。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>收集 key 的依赖</title> </head> <body> <script> let activeEffect;
const data = { ok: true, text: 'hello world' }
const bucket = new WeakMap()
const obj = new Proxy(data, { get(target, key) { track(target, key) return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) } })
function track(target, key) { if (!activeEffect) return 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)
activeEffect.deps.push(deps) }
function trigger(target, key) { const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key) const effectsToRun = new Set(effects)
effectsToRun && effectsToRun.forEach(fn => fn()) }
function effect(fn) { function effectFn() { activeEffect = effectFn
cleanup(effectFn) fn() }
effectFn.deps = []
effectFn() }
function cleanup(effectFn) {
console.log('effectFn ', effectFn.deps) for(let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i]
deps.delete(effectFn) }
effectFn.deps.length = 0 }
effect(() => { console.log(' obj.ok', obj.ok) document.body.innerHTML = obj.ok ? obj.text : 'ok not' })
setTimeout(() => { obj.ok = false
console.log('bucket ', bucket) }, 1000)
</script> </body> </html>
|