通过 Proxy、WeakMap 和 Dep 实现 reactive 的依赖收集、更新派发、重复代理处理和 ref 自动解包
前面实现了 ref、effect 以及依赖链表的更新流程。
这一篇开始 reactive。它和 ref 的区别在于:ref 可以把依赖直接挂在实例上,而 reactive 代理的是一个普通对象,对象里的每个属性都需要独立收集依赖。
基础结构
先写一个最小版本。
reactive 不直接创建 Proxy,而是交给 createReactiveObject 处理:
import { isObject } from '@vue/shared'
export function reactive(target: object) {
return createReactiveObject(target)
}
function createReactiveObject(target: object) {
if (!isObject(target)) {
return target
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
trigger(target, key)
return Reflect.set(target, key, value, receiver)
}
})
return proxy
}
function track(target, key) {
}
function trigger(target, key) {
}
通过 createReactiveObject 包一层,是因为后面不只有 reactive,还会有 shallowReactive 这类不同的代理创建方式。
现在核心问题变成了:
- 在
get时如何收集依赖 - 在
set时如何派发更新
reactive 的依赖存在哪里
回到 ref 的实现,它是通过实例上的 subs 和 subsTail 保存订阅者:
class Dep {
subs: Link
subsTail: Link
}
收集依赖时,只要拿到这个 dep,就可以继续复用之前的链表逻辑:
function track(target, key) {
// if (activeSub) {
// link(dep, activeSub)
// }
}
function trigger(target, key) {
// if (dep.subs) {
// propagate(dep.subs)
// }
}
问题在于,reactive 代理的是普通对象,不能像 ref 一样把 Dep 直接挂在实例上。
而且一个对象可能有多个属性,每个属性都要有自己的 Dep:
const target = {
a: 1,
b: 2
}
所以需要一个额外的数据结构,把 target、key 和 Dep 关联起来。
使用 WeakMap
使用 WeakMap 保存对象和依赖之间的关系。
WeakMap 的特点是:
key只能是对象,value可以是任意类型- 它对
key是弱引用 - 如果一个对象只被
WeakMap作为key引用,并且程序里没有其他地方再使用它,这个对象就可以被垃圾回收
如果使用普通 Map,Map 会一直强引用 target。即使这个对象已经不再使用,只要它还在 Map 里,就不能被回收,容易造成内存泄漏。
最终结构是:
targetMap:全局WeakMap,key是原始对象targetdepsMap:每个对象对应一个Map,key是对象属性dep:每个属性对应一个Dep,保存订阅这个属性的effect
可以理解成下面这个结构:

targetMap = {
[target]: {
a: Dep,
b: Dep
}
}
这样就可以通过 target 找到对应的 depsMap,再通过属性名找到对应的 Dep。
收集依赖
track 负责在读取属性时建立依赖关系。
function track(target: object, key: unknown) {
if (!activeSub) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Dep()))
}
link(dep, activeSub)
}
流程是:
- 如果当前没有正在执行的
effect,直接跳过 - 根据
target从targetMap中取出depsMap - 如果不存在,就为这个对象创建一个新的
depsMap - 根据
key从depsMap中取出dep - 如果不存在,就为这个属性创建一个新的
Dep - 调用
link(dep, activeSub)建立订阅关系
派发更新
trigger 负责在属性修改时找到之前收集好的 dep,再通知订阅者更新。
function trigger(target: object, key: unknown) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const dep = depsMap.get(key)
if (!dep) {
return
}
propagate(dep.subs)
}
更新流程和收集流程是对应的:
- 根据
target找到depsMap - 根据
key找到dep - 通过
propagate(dep.subs)派发更新
reactive 的其他情况
避免重复代理
同一个原始对象多次传入 reactive 时,应该返回同一个代理对象:
it('同一个对象多次 reactive', () => {
const obj = {
a: 1
}
const state1 = reactive(obj)
const state2 = reactive(obj)
expect(state1).toBe(state2)
})
可以增加一个 reactiveMap,用来保存 target -> proxy 的关系:
const reactiveMap = new WeakMap()
function createReactiveObject(target: object) {
const existProxy = reactiveMap.get(target)
if (existProxy) {
return existProxy
}
const proxy = new Proxy(target, {
// ...
})
reactiveMap.set(target, proxy)
return proxy
}
这样同一个原始对象只会创建一次代理。
处理代理对象再次传入
还需要处理另一种情况:已经是代理对象了,又被传入 reactive。
it('代理对象再次传入 reactive', () => {
const obj = {
a: 1
}
const state1 = reactive(obj)
const state2 = reactive(state1)
expect(state1).toBe(state2)
})
如果不处理,就可能变成这样的结构:
state2 -> Proxy -> state1 -> Proxy -> obj
ref 可以通过自身的 IS_REF 标记判断是不是 ref。reactive 是通过 Proxy 实现的,也可以在代理的 get 拦截里返回一些特殊标记。
import type { Link } from './system'
import { isObject } from '@vue/shared'
import { activeSub } from './effect'
import { link, propagate } from './system'
const reactiveMap = new WeakMap<Target, any>()
export enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
RAW = '__v_raw'
}
export function reactive(target: object) {
return createReactiveObject(target)
}
function createReactiveObject(target: object) {
if ((target as Target)[ReactiveFlags.RAW]) {
return target
}
const existProxy = reactiveMap.get(target)
if (existProxy) {
return existProxy
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
if (key === ReactiveFlags.RAW) {
return target
}
track(target, key)
return Reflect.get(target, key, receiver)
}
})
reactiveMap.set(target, proxy)
return proxy
}
export interface Target {
[ReactiveFlags.IS_REACTIVE]?: boolean
[ReactiveFlags.RAW]?: any
}
const targetMap: WeakMap<Target, any> = new WeakMap<Target, any>()
export function isReactive(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}
这里的 __v_isReactive 和 __v_raw 并不是真实存在于对象上的属性,而是通过 Proxy 的 get 拦截器返回的特殊值:
- 读取
__v_isReactive时返回true - 读取
__v_raw时返回原始对象target
所以当一个代理对象再次传入 reactive 时,读取 ReactiveFlags.RAW 会触发代理对象的 get
,从而返回它对应的原始对象。这里就可以判断它已经是一个代理对象,直接返回当前值。
相同值不触发更新
如果给对象属性设置相同的值,不应该重复触发更新。
it('相同的值不触发更新', () => {
const obj = {
a: 1
}
const state = reactive(obj)
const fn = vi.fn()
effect(() => {
state.a
fn()
})
state.a = 1
expect(fn).toBeCalledTimes(1)
})
在 set 中先保存旧值,真正写入后再比较新旧值:
set(target, key, value, receiver)
{
const oldValue = target[key]
const res = Reflect.set(target, key, value, receiver)
if (hasChanged(oldValue, value)) {
trigger(target, key)
}
return res
}
export function hasChanged(value: any, oldValue: any): boolean {
return !Object.is(value, oldValue)
}
reactive 中自动解包 ref
当 ref 作为对象属性放进 reactive 时,读取属性时不需要再手动 .value:
it('ref 嵌入 reactive', () => {
const count = ref(0)
const state = reactive({ count })
const fn = vi.fn()
effect(() => {
state.count
fn()
})
expect(state.count).toBe(0)
state.count = 2
expect(fn).toBeCalledTimes(2)
expect(count.value).toBe(2)
})
这个测试希望满足三个行为:
- 读取
state.count时自动返回count.value - 修改
state.count后可以正常触发更新 - 修改
state.count后,原来的count.value也会同步更新
读取时可以在 get 中判断结果是不是 ref:
get(target, key, receiver)
{
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
if (key === ReactiveFlags.RAW) {
return target
}
track(target, key)
const res = Reflect.get(target, key, receiver)
if (isRef(res)) {
return res.value
}
return res
}
写入时,如果旧值是 ref,新值不是 ref,就不要替换整个属性,而是写入旧 ref 的 .value:
set(target, key, value, receiver)
{
const oldValue = target[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const res = Reflect.set(target, key, value, receiver)
if (hasChanged(oldValue, value)) {
trigger(target, key)
}
return res
}
这样 state.count = 2 实际更新的是 count.value,所以原来的 ref 对象仍然保持关联。
嵌套对象响应式
it('嵌套的 reactive 对象', () => {
const obj = {
a: {
b: 1
}
}
const state = reactive(obj)
const fn = vi.fn()
effect(() => {
state.a.b
fn()
})
state.a.b = 2
expect(fn).toBeCalledTimes(2)
})
读取 state.a.b 时,第一次读取的是 state.a。如果 a 仍然只是一个普通对象,那么后续修改 a.b 时不会触发代理对象的
set。
所以在 get 中,如果读取结果是对象,需要继续调用 reactive:
get(target, key, receiver)
{
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
if (key === ReactiveFlags.RAW) {
return target
}
track(target, key)
const res = Reflect.get(target, key, receiver)
if (isRef(res)) {
return res.value
}
if (isObject(res)) {
return reactive(res)
}
return res
}
这样嵌套对象会在读取时被懒代理。只有真正访问到 state.a 时,才会把 a 转成响应式对象。