
上一次我们提到:
- 每个对象的每个属性都需要自己的
Dep
。 - 如何建立
target.a
→Dep
的对应关系? - 如何在不污染原始对象的情况下存储这个关系?
我们可以先来做一个简单的比较。
Ref 与 Reactive 比较
Ref | Reactive | |
---|---|---|
数据结构 | 单一值 | 对象(多个属性) |
依赖存储 | 直接在实例上 (this ) |
需要一个外部的存储机制 |
依赖数量 | 一个 ref 对应一个 Dep |
一个对象对应多个 Dep (每个属性一个) |
匯出到試算表
Ref
可以用 this
来存储依赖,是因为它本身就是一个封装了 subs
和 subsTail
属性的实例。
但 Reactive
的每个属性都需要自己的 Dep
,那要存在哪里呢?这时候我们就可以使用 WeakMap
对象。
什么是 WeakMap?
WeakMap
是一种键值对集合 (key-value pairs)。- key 只能是对象(不能是字符串、数字、布尔值),value 可以是任意类型。
- 弱引用 (weak reference) :如果一个对象只被
WeakMap
作为 key 使用,而程序中没有其他变量引用它,这个对象就会被垃圾回收 (GC) 自动清除。
看来它正好适合我们用来建立依赖的关联关系。
核心概念
我们使用 WeakMap
创建一个全局的 targetMap
,它的三层嵌套结构如下:
- 第一层
targetMap
(WeakMap) :key
是原始的目标对象target
,value
是第二层的depsMap
。{ target => depsMap }
- 第二层
depsMap
(Map) :key
是target
对象中的属性名key
,value
是第三层的dep
。{ key => dep }
- 第三层
dep
(Dep 实例) :依赖的容器,存储了所有订阅该属性变更的effect
。{ subs, subsTail }
为何不直接用 Map?
因为如果使用普通的 Map
,Map
会一直保持对 target
对象的强引用。只要 target
还存在于 Map
中,GC 就无法回收它,即使程序其他地方已经不再使用这个 target
,从而导致内存泄漏。
JavaScript
const targetMap = new WeakMap()
它的结构会长这样:
JavaScript
target = {
a: 0,
b: 1
}
targetMap = {
[target]: { // WeakMap 的 key 是 target 对象本身
'a': Dep, // Map 的 key 是属性名 'a'
'b': Dep // Map 的 key 是属性名 'b'
}
}
这样 Dep
和 target
的属性就有关系了。我们可以通过 target
找到对应的 Map
,再通过属性 a
找到对应的 Dep
实例,这样就可以建立关联关系。
等到需要触发更新时,通过 target
找到对应的 Map
,再通过 key
找到对应的 Dep
来通知更新。

收集依赖
收集依赖分为首次收集和后续收集,所以我们可以这样写。
TypeScript
function track(target, key){
if (!activeSub) return
// 通过 targetMap 获取 target 的依赖合集 (depsMap)
let depsMap = targetMap.get(target)
// 首次收集依赖,如果之前没有收集过,就新建一个
// key: target (obj) / value: depsMap (new Map())
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 获取属性对应的 Dep,如果不存在则新建一个
let dep = depsMap.get(key)
// key: key (property name) / value: new Dep()
if (!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
console.log(targetMap, dep)
link(dep, activeSub)
}
可以 console.log
看一下 targetMap
和 dep
:
看起来确实是我们想的那样,接下来实现触发更新。
触发更新
触发更新时,我们理应去 targetMap
中寻找之前存储的 dep
:
TypeScript
function trigger(target, key){
const depsMap = targetMap.get(target)
// 如果 depsMap 不存在,表示没有任何依赖被收集过,直接返回
if (!depsMap) return
const dep = depsMap.get(key)
// 如果 dep 不存在,表示这个 key 没有在 effect 中被使用过,直接返回
if (!dep) return
// 找到依赖,触发更新
if (dep.subs) {
propagate(dep.subs)
}
}
接下来回头看我们的示例,初始化成功输出 0,一秒之后输出 1。
看起来成功了。接下来我们来测试
reactive
中 getter
的响应式追踪:
JavaScript
//import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, effect } from '../dist/reactivity.esm.js'
const state = reactive({
a: 0,
get count(){
return this.a
}
})
effect(() => {
console.log(state.count)
})
setTimeout(() => {
state.a = 1
}, 1000)
预期结果是先输出 0,一秒后输出 1。但实际执行后,我们发现只有初始的 0 被输出,state.a = 1
的更新并未触发 effect
。
通过在 track
函数中输出 (target, key)
,发现只有 count
属性被追踪了,a
属性并没有。
原因在于 return this.a
。在 getter
内部,this
默认指向的是原始的 target
对象 ,而不是我们的 proxy
对象。
因此 this.a
的取值过程绕过了 Proxy
的 get
处理器,a
属性的依赖自然也无法被收集。
JavaScript
csharp
const state = reactive({
a: 0,
get count() {
return this.a // 这里的 this 应该指向谁?
}
})
它应该指向我们的 Proxy
对象而不是原始对象,这样它在触发 getter
的时候,才会再次进入 Proxy
的 get
处理器去执行 track(target, key)
。
那我们要怎么做?
Proxy
的 handler
提供了第三个参数 receiver
,它指向的就是 proxy
对象本身。因此我们只需要将它传递给 Reflect.get
就可以修正 this
的指向。
TypeScript
function createReactiveObject(target) {
// reactive 只处理对象
if (!isObject(target)) return target
// 创建 target 的代理对象
const proxy = new Proxy(target, {
get(target, key, receiver) { // 接收第三个参数 receiver
// 收集依赖:绑定 target 的属性与 effect 的关系
track(target, key)
// 将 receiver 传给 Reflect.get
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) { // 接收第四个参数 receiver
const res = Reflect.set(target, key, newValue, receiver)
// 触发更新:通知之前收集的依赖,重新执行 effect
trigger(target, key)
return res
}
})
return proxy
}
这样我们就完成了,也可以看到初始化时 console.log
输出 0,一秒之后输出 1。
执行步骤
回顾我们今天:
- 我们引入了以
WeakMap
为核心的targetMap
数据结构,解决了在不污染原始对象的前提下,为多属性对象管理各自依赖的问题。 - 我们实现了与
targetMap
配套的track
和trigger
函数。 - 我们利用
Proxy
的receiver
参数,修正了getter
中this
指向的问题。
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。