
在完成 reactive
的基本实现之后,接下来会遇到几种常见且必须处理的情况:
- 原始对象传入 Reactive 对象
- Reactive 对象再次传入 Reactive
- 对 Reactive 对象重复赋相同数值
- 嵌套 对象作为
ref
的值 - 将包含
ref
的 Reactive 对象进行解构并保持数值同步 - 初始化嵌套 Reactive 对象
情况一:原始对象传入 Reactive 对象
这是最基础也最直观的案例。
如果把同一个原始对象 多次传入 reactive
,当前的简化版本会返回不同的 Proxy 实例。
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
<style>
body {
padding: 150px;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module">
// import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, effect } from '../dist/reactivity.esm.js'
const obj = {
a:0
}
const state = reactive(obj)
const state2 = reactive(obj)
console.log(state === state2)
effect(() => {
console.log(state.a)
})
setTimeout(() => {
state.a = 1
}, 1000)
</script>
</body>
</html>

当我们将同一个原始对象 多次传入 reactive
函数时,会发现返回的代理对象彼此不相等 (state !== state2
),这与官方的行为(返回相等的代理对象)不符。
为什么会出现 state !== state2
?原因在于目前的 createReactiveObject
函数,每次调用都会无条件 地 new Proxy()
一个新的代理对象。
vbnet
function createReactiveObject(target) {
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, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver)
trigger(target, key)
return res
}
})
return proxy
}
因此需要加入缓存机制 ,避免让相同对象被重复代理。
vbnet
/**
* 存储 target 与响应式对象的关联关系
* key: target / value: proxy
*/
const reactiveMap = new WeakMap()
function createReactiveObject(target) {
// reactive 只处理对象
if (!isObject(target)) return target
// 如果这个 target 已经被 reactive 过了,直接返回已创建的 proxy
const existingProxy = reactiveMap.get(target)
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key,receiver)
},
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver)
trigger(target, key)
return res
}
})
// 缓存 target 与响应式对象的关联
reactiveMap.set(target, proxy)
return proxy
}
如果没有缓存,会导致:
- 内存浪费:重复创建无用的代理对象。
- 依赖分裂 :两个不同的 proxy 操作同一个 target,但各自的依赖收集不一致,可能导致更新失效或重复触发。
情况二:Reactive 对象传入 Reactive
xml
<body>
<div id="app"></div>
<script type="module">
// import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, effect } from '../dist/reactivity.esm.js'
const obj = {
a:0
}
const state = reactive(obj)
const state2 = reactive(state)
console.log(state === state2)
</script>
</body>
在官方实现中,预期结果为 true
。因此我们需要在再次传入已是 Proxy 的对象 时,仍然返回缓存的代理对象。
官方的做法是在代理对象的 get
中拦截访问某个特殊属性 来识别并返回缓存;我们也可以采用更直接的办法:引入 reactiveSet
记录所有通过 reactive
创建的代理对象,防止重复代理。
scss
/**
* 保存所有使用 reactive 创建的响应式对象
* 用于检查是否被重复 reactive
*/
const reactiveSet = new Set()
function createReactiveObject(target) {
// reactive 只处理对象
if (!isObject(target)) return target
// 如果 target 已存于 reactiveSet 中,说明它本身就是一个 proxy
// 直接通过 reactiveMap 取回对应的"已缓存代理"
if (reactiveSet.has(target)) {
return reactiveMap.get(target)
}
...
...
})
// 存储 target 与 proxy 的关联
reactiveMap.set(target, proxy)
// 记录该 proxy 已是响应式对象
reactiveSet.add(proxy)
return proxy
}
// 判断 target 是否为响应式对象:只要在 reactiveSet 中存在即为 true
export function isReactive(target) {
return reactiveSet.has(target)
}
重点是避免重复代理 :若传入的已经是 proxy
,就应该原样返回。
注:Vue 官方常见做法是通过
Proxy
的get
对_v_isReactive
之类的内部标记进行判断;我们这里用reactiveSet
简化了识别逻辑。本质上都是在区分 target 与 proxy 的身份,以避免陷入无穷无尽的代理链。
情况三:Reactive 对象重复赋相同数值
xml
<body>
<div id="app"></div>
<script type="module">
// 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
})
effect(() => {
console.log(state.a)
})
setTimeout(() => {
state.a = 0
}, 1000)
</script>
</body>
按上述实现,给相同数值赋值时,控制台会重复输出两次;但官方只会输出一次 。
原因是官方会检查新旧值是否相同,相同则不触发更新。

因此我们需要在 set
时判断新旧值是否发生变化 ,以避免不必要的通知。
先在 @vue/shared
新增一个辅助函数,用于判断值是否变化:
javascript
// shared.ts
export function isObject(value) {
return typeof value === 'object' && value !== null
}
// 判断新值与旧值是否发生变化;变化返回 true,否则返回 false
export function hasChanged(newValue, oldValue) {
return !Object.is(newValue, oldValue)
}
在 reactive.ts
中引入并使用:
javascript
// reactive.ts
import { isObject, hasChanged } from '@vue/shared'
...
...
set(target, key, newValue, receiver) {
const oldValue = target[key]
const res = Reflect.set(target, key, newValue, receiver)
if (hasChanged(newValue, oldValue)) {
// 仅当值确实变化时才触发更新
trigger(target, key)
}
return res
}
...
情况四:嵌套对象 传入 ref
为避免多层嵌套对象直接作为 ref
的值后失去响应 ,当传入 ref
的值是对象时,应先转换成 reactive
。
kotlin
// ref.ts
import { isObject } from '@vue/shared'
import { reactive } from './reactive'
enum ReactiveFlags {
IS_REF = '__v_isRef'
}
class RefImpl {
_value;
[ReactiveFlags.IS_REF] = true
subs: Link
subsTail: Link
constructor(value) {
// 如果 value 是对象,则先转为响应式对象
this._value = isObject(value) ? reactive(value) : value
}
get value() {
if (activeSub) {
trackRef(this)
}
return this._value
}
set value(newValue) {
// 若新旧值确实发生变化,再更新
if (hasChanged(newValue, this._value)) {
// 若新值是对象,同样转成 reactive
this._value = isObject(newValue) ? reactive(newValue) : newValue
triggerRef(this)
}
}
}
这样,当 ref
的值是对象时,内部会被转为 reactive
,从而继续参与依赖收集与触发。
情况五:将包含 ref
的 Reactive 对象解构并保持同步
为了正确处理 ref
与 reactive
的整合,我们需要满足三点:
ref
传入reactive
后,读取时可直接拿到值 (无需.value
)
xml
<body>
<div id="app"></div>
<script type="module">
// import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, ref, effect } from '../dist/reactivity.esm.js'
// 如果 target.a 是一个 ref,就应当直接拿到它的值,而不是 .value
const a = ref(0)
const state = reactive({ a })
effect(() => {
// 不用 state.a.value 也可以读取
console.log('reactive', state.a)
})
</script>
</body>

ref
传入reactive
后,当reactive
更新同名字段时,ref.value
也要同步更新
xml
<body>
<div id="app"></div>
<script type="module">
// import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, ref, effect } from '../dist/reactivity.esm.js'
const a = ref(0)
const state = reactive({ a })
effect(() => {
console.log('reactive', state.a)
})
setTimeout(() => {
// 更新 reactive 字段,ref.value 需要同步
state.a = 1
console.log('ref', a.value)
}, 1000)
</script>
</body>

- 若把
state.a
直接换成一个新的ref
,原有变量a
不应被动同步(这是预期的非同步)
xml
<body>
<div id="app"></div>
<script type="module">
// import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, ref, effect } from '../dist/reactivity.esm.js'
const a = ref(0)
const state = reactive({ a })
effect(() => {
console.log('reactive', state.a)
})
setTimeout(() => {
// 将字段替换为"新的 ref",原来的 a 不同步(预期行为)
state.a = ref(1)
console.log('ref', a.value)
}, 1000)
</script>
</body>

实现要点
ref
传入reactive
,读取时自动解.value
kotlin
// reactive.ts
...
...
get(target, key, receiver) {
// 收集依赖:绑定 target[key] 与当前 effect 的关系
track(target, key)
const res = Reflect.get(target, key, receiver)
// 若是 ref,则返回其 .value
if (isRef(res)) {
return res.value
}
return res
},
...
...
这样即可做到读取直出数值 ,但 a
内部的 value
仍需在赋值时同步更新。
- 在 Proxy 的 setter 中判断新旧值并决定是否触发
ref
更新
首先在 @vue/shared
中导出 hasChanged
(前文已给出实现),然后在 set
中处理**老值为 ref
、新值不是 ref
**的情形:
scss
set(target, key, newValue, receiver) {
const oldValue = target[key]
/**
* const a = ref(0)
* target = { a }
* 当执行 target.a = 1 时,本质上是 a.value = 1
*/
if (isRef(oldValue) && !isRef(newValue)) {
oldValue.value = newValue
// 更新了 ref 的值,已经触发了依赖
// 直接返回,避免下方 trigger 再触发一次(双重触发)
return true
}
const res = Reflect.set(target, key, newValue, receiver)
if (hasChanged(newValue, oldValue)) {
// 仅当值变化时才触发更新
trigger(target, key)
}
return res
}
情况六:初始化嵌套 Reactive 对象
xml
<body>
<div id="app"></div>
<script type="module">
// import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { ref, reactive, effect } from '../dist/reactivity.esm.js'
const state = reactive({
a: {
b: 0
}
})
effect(() => {
console.log(state.a.b)
})
setTimeout(() => {
state.a.b = 1
}, 1000)
</script>
</body>
运行后会发现,当 state.a.b
被修改时,effect
没有重新触发。
原因是:属性 a
的值(红色部分)本身不是响应式对象,只有外层对象(橙色)被代理了。

get
逻辑示例(初始版本):
vbnet
get(target, key, receiver) {
track(target, key)
const res = Reflect.get(target, key, receiver)
console.log(res)
if (isRef(res)) {
return res.value
}
return res
}
在 trigger
中打印会发现没有响应 ,因为只有被代理的对象才具备依赖映射,非响应式对象的属性变更不会触发:
javascript
export function trigger(target, key) {
console.log('trigger', target, key)
...
}
// ------没有任何反应
解决办法 :在 get
中若读取到的 res
仍然是对象,则惰性地把它再转成响应式对象(递归式"按需响应")。
scss
get(target, key, receiver) {
track(target, key)
const res = Reflect.get(target, key, receiver)
if (isRef(res)) {
return res.value
}
if (isObject(res)) {
/**
* 如果 res 是对象,则将其转为响应式对象(惰性转换)
*/
return reactive(res)
}
return res
}
重构:抽离 handlers
,减少重复创建
为提升性能 并遵循单一职责原则 ,应将 Proxy
的拦截处理抽离为独立模块,使所有代理实例共享同一份 handlers ,避免每次 createReactiveObject
都重新创建对象。
baseHandlers.ts
javascript
import { hasChanged, isObject } from '@vue/shared'
import { track, trigger } from './dep'
import { isRef } from './ref'
import { reactive } from './reactive'
export const mutableHandlers = {
get(target, key, receiver) {
// 收集依赖:绑定 target[key] 与当前 effect
track(target, key)
const res = Reflect.get(target, key, receiver)
// 若为 ref,直接返回其 value
if (isRef(res)) {
// target = { a: ref(0) }
return res.value
}
// 若为对象,惰性地转为响应式对象
if (isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, newValue, receiver) {
const oldValue = target[key]
/**
* const a = ref(0)
* target = { a }
* 更新 target.a = 1 等价于 a.value = 1
*/
if (isRef(oldValue) && !isRef(newValue)) {
oldValue.value = newValue
// 设置 ref.value 已触发依赖;避免二次 trigger
return true
}
const res = Reflect.set(target, key, newValue, receiver)
if (hasChanged(newValue, oldValue)) {
// 值变化才触发更新
trigger(target, key)
}
return res
}
}
dep.ts
javascript
import { Link, link, propagate } from "./system"
import { activeSub } from "./effect"
class Dep {
subs: Link
subsTail: Link
constructor() {}
}
const targetMap = new WeakMap()
export function track(target, key) {
if (!activeSub) return
// 通过 targetMap 取得 target 的依赖映射
let depsMap = targetMap.get(target)
// 首次收集依赖:若不存在则新建
// key: obj / value: depsMap
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
// 第一次建立 key 对应的 Dep,并保存到 depsMap
// key: key / value: Dep
if (!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
link(dep, activeSub)
}
export function trigger(target, key) {
const depsMap = targetMap.get(target)
// 未收集过依赖则直接返回
if (!depsMap) return
const dep = depsMap.get(key)
// 该 key 从未在 effect 中被使用过
if (!dep) return
// 触发依赖链
propagate(dep.subs)
}
reactive.ts
javascript
import { isObject } from '@vue/shared'
import { mutableHandlers } from './baseHandlers'
/**
* 存储 target 与响应式对象的关联关系
* key: target / value: proxy
*/
const reactiveMap = new WeakMap()
/**
* 保存所有通过 reactive 创建的响应式对象
* 用于检查是否重复 reactive
*/
const reactiveSet = new Set()
function createReactiveObject(target) {
// reactive 只处理对象
if (!isObject(target)) return target
// 若 target 已是 proxy,则直接返回缓存的 proxy
if (reactiveSet.has(target)) {
return reactiveMap.get(target)
}
// 若该 target 已被代理过,复用已存在的 proxy
const existingProxy = reactiveMap.get(target)
if (existingProxy) {
return existingProxy
}
// 创建 target 的代理对象
const proxy = new Proxy(target, mutableHandlers)
// 记录 target ↔ proxy
reactiveMap.set(target, proxy)
// 标记该 proxy 为响应式对象
reactiveSet.add(proxy)
return proxy
}
export function reactive(target) {
return createReactiveObject(target)
}
// 判断对象是否为响应式对象(是否在 reactiveSet 中)
export function isReactive(target) {
return reactiveSet.has(target)
}
小结:六大情境背后的设计取舍
- 缓存机制:避免重复代理与依赖分裂。
- 身份识别 :区分原始对象、代理对象与
ref
。 - 性能优化 :仅在值发生变化时触发更新。
- API 体验 :在
reactive
中自动解构ref.value
,让读取更直觉。 - 惰性策略:按需把嵌套对象转为响应式,提升初始化性能。
- 工程化 :抽离
handlers
,提高复用性与可维护性。
这些选择本质上是在性能、易用性与一致性 之间做权衡。理解这些"边界案例",不仅能写出可用的响应式系统,更能洞察 Vue 3 背后的设计思路。
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。