响应式系统实现
响应式基础
通过 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>
|