reactive 和 effect 衔接
单独看 reactive,本质上就是一层 Proxy 外壳:
get能拦截读取set能拦截修改has、ownKeys、deleteProperty这些也能拦截
但是只有拦截还不够。
如果只是“知道有人读了”“知道有人改了”,而不知道:
- 是谁在读
- 读的是哪个对象的哪个属性
- 改了之后应该通知谁
那这套系统还是跑不起来。
所以接下来真正要做的,不是继续往 reactive 里塞逻辑,而是把 reactive 和 effect 接起来。
真正要解决的问题只有一句话:
读取属性的时候,把当前 effect 记下来;修改属性的时候,把之前记下来的 effect 找出来重新执行。
围绕这句话,整体设计就顺出来了:
effect(fn)执行时,要记录当前正在运行的是哪个 effectreactive的get拦截里调用track()进行依赖收集track()要把target、key和当前 effect 建立关系reactive的set拦截里调用trigger()trigger()再根据target和key把相关 effect 找出来执行,也就是派发更新
其实响应式主干就这么几步。
对这几个角色的理解
Section titled “对这几个角色的理解”reactive
Section titled “reactive”reactive 负责把普通对象变成代理对象。
它本身不负责存依赖,也不负责调度 effect。 它更像“入口壳子”,把读写操作都拦下来,然后把事情转交给:
tracktrigger
也就是说,reactive 做的是“把拦截点暴露出来”。
effect
Section titled “effect”effect 负责提供“当前上下文”。
如果没有 effect,track() 根本不知道当前是谁在读数据。
所以 effect(fn) 的核心职责不是“执行函数”这么简单,而是:
- 执行前,把当前
effect设成active状态 - 执行函数
- 执行结束后,再把
active状态恢复
这样 track() 才能知道:
现在这个读取,是哪个
effect触发的
track 负责收集依赖。
它关心的是三件事:
- 哪个对象
- 哪个 key
- 哪个 effect 依赖了这个 key
trigger
Section titled “trigger”trigger 负责派发更新。
它做的事情正好和 track 相反:
- 已知哪个对象的哪个 key 变了
- 去找到之前依赖它的 effect
- 然后重新执行这些 effect
为什么依赖结构要设计成 WeakMap -> Map -> Set
Section titled “为什么依赖结构要设计成 WeakMap -> Map -> Set”WeakMap target1 -> Map key1 -> Set(effect1, effect2) key2 -> Set(effect3)
target2 -> Map key1 -> Set(effect4)现在把它拆开理解会更顺一点。
第一层:WeakMap
Section titled “第一层:WeakMap”第一层用来存“对象级别”的依赖关系。
key 是原始对象 target,value 是一个 Map。
之所以用 WeakMap,是因为 key 是对象,而且对象如果以后没人引用了,应该允许它被垃圾回收,不想因为依赖表把它强行留住。
这一层回答的是:
这个对象身上,哪些属性是有依赖的?
第二层:Map
Section titled “第二层:Map”这一层是某个对象内部的“属性依赖表”。
key 是具体属性,比如:
'count''name''length'ITERATE_KEY
value 是一个 Set。
这一层回答的是:
这个对象的哪个 key 被依赖了?
第三层:Set
Section titled “第三层:Set”这里面放的就是真正的 effect。
比如:
effect(() => { console.log(state.count)})当 state.count 被读取时,这个 effect 就会被放进 count 对应的 Set 里。
之所以用 Set,主要是为了去重。
如果同一个 effect 在一次执行里多次读到同一个 key,最终也只应该收集一次。
这一层回答的是:
依赖这个 key 的 effect 有哪些?
为什么不是直接 target -> effect
Section titled “为什么不是直接 target -> effect”因为响应式不是“对象级别整体更新”,而是“尽量按 key 精准更新”。
比如:
const state = reactive({ count: 0, name: 'vue',})
effect(() => { console.log(state.count)})这里 effect 只依赖 count,不依赖 name。
如果只存成:
target -> Set(effect)那后面改 name 的时候,也会把依赖 count 的 effect 触发掉。这样粒度太粗了。
所以必须多一层 key -> Set(effect)。
一次最小闭环到底是怎么跑起来的
Section titled “一次最小闭环到底是怎么跑起来的”例子:
const state = reactive({ count: 0 })
effect(() => { console.log(state.count)})
state.count++这段代码的执行过程,可以拆成两段。
第一段:注册依赖
Section titled “第一段:注册依赖”先执行:
effect(() => { console.log(state.count)})过程大概是这样:
effect(fn)被调用- 内部把当前 effect 记录下来,比如记到
activeEffect - 然后立刻执行这个
fn fn里读取了state.count- 读取
state.count会触发Proxy的get get里调用track(target, TrackOpTypes.GET, key)track()发现当前有activeEffect- 于是把这条关系记到依赖表里
最后得到的关系就是:
targetMap rawState -> Map 'count' -> Set(currentEffect)这一步的重点不是更新,而是“先把依赖登记好”。
第二段:触发更新
Section titled “第二段:触发更新”然后执行:
state.count++这一步本质上会触发 set。
大概流程是:
set被触发set里判断这是SET还是ADD- 然后调用
trigger(target, TriggerOpTypes.SET, key) trigger()根据target找到depsMap- 再根据
key找到对应的Set(effect) - 把里面的 effect 重新执行
这样前面那个:
effect(() => { console.log(state.count)})就会再跑一遍。
这时页面、控制台或者别的依赖结果,也就跟着更新了。