手写 reactive
reactive 创建一个响应式的对象
import { isObject } from '../utils'
/**
* 实现一个reactive函数,用于创建响应式对象
*/
export function reactive(obj) {
// 首先判断参数是不是对象
if (!isObject(obj)) {
return obj
}
return new Proxy(obj, {
get(target, key, receiver) {
const value = target[key]
console.log(`读取${key}属性, 属性值为${value}`)
return value
},
set(target, key, value, receiver) {
console.log(`将${key}属性值设置为${value}`)
return target[key] = value
}
})
}示例
<template>
<div ref="wrapper"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { reactive } from '../source/reactivity'
const wrapper = ref(null)
const state = reactive({
name: '小李大人',
age: 18
})
console.log(state)
onMounted(() => {
wrapper.value.innerHTML = `姓名:${state.name},年龄:${state.age}`
setTimeout(() => {
state.age++
}, 1000)
})
</script>控制台输出: 
到这里一个响应式的对象已经被成功创建
重复代理同一个对象
上一节中有一个bug,同一个对象会被多次代理,如下:
<template>
<div ref="wrapper"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { reactive } from '../source/reactivity'
const wrapper = ref(null)
const obj = {
name: '小李大人',
age: 18
}
const state1 = reactive(obj)
const state2 = reactive(obj)
console.log(state1)
console.log(state2)
console.log(state1 === state2)
// onMounted(() => {
// wrapper.value.innerHTML = `姓名:${state.name},年龄:${state.age}`
// setTimeout(() => {
// state.age++
// }, 1000)
// })
</script>控制台输出: 
可以看到同一个对象obj是被重复代理了,通过加入缓存来解决重复代理同一个对象的问题:
通过加入缓存来解决重复代理同一个对象的问题:
import { isObject } from '../utils'
/** 缓存代理过的对象 */
const reactiveMap = new WeakMap()
const mutableHandlers = {
get(target, key, receiver) {
const value = target[key]
console.log(`读取${key}属性, 属性值为${value}`)
return value
},
set(target, key, value, receiver) {
console.log(`将${key}属性值设置为${value}`)
return target[key] = value
}
}
/** 实现一个reactive函数,用于创建响应式对象 */
export function reactive(obj) {
// 首先判断参数是不是对象
if (!isObject(obj)) {
return obj
}
// 如果代理过,则直接返回代理过的对象
if (reactiveMap.has(obj)) {
return reactiveMap.get(obj)
}
const proxy = new Proxy(obj, mutableHandlers)
// 缓存代理过的对象
reactiveMap.set(obj, proxy)
return proxy
}示例
<template>
<div ref="wrapper"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { reactive } from '../source/reactivity'
const wrapper = ref(null)
const obj = {
name: '小李大人',
age: 18
}
const state1 = reactive(obj)
const state2 = reactive(obj)
console.log(state1)
console.log(state2)
console.log(state1 === state2)
// onMounted(() => {
// wrapper.value.innerHTML = `姓名:${state.name},年龄:${state.age}`
// setTimeout(() => {
// state.age++
// }, 1000)
// })
</script>控制台输出: 
这里成功的解决了同一个对象被多次代理的问题
同一个对象被代理后,再去代理它的代理对象,也会被重复代理
上一节中解决了同一个对象被多次代理的问题,但是同一个对象被代理后,再去代理它的代理对象,也会被重复代理
示例
<template>
<div ref="wrapper"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { reactive } from '../source/reactivity'
const wrapper = ref(null)
const obj = {
name: '小李大人',
age: 18
}
const state1 = reactive(obj)
const state2 = reactive(state1)
console.log(state1)
console.log(state2)
console.log(state1 === state2)
// onMounted(() => {
// wrapper.value.innerHTML = `姓名:${state.name},年龄:${state.age}`
// setTimeout(() => {
// state.age++
// }, 1000)
// })
</script>控制台输出: 
为了解决这个问题:
- 需要在代理对象之前,先判断这个对象是否已经被代理过
- 如果已经被代理过,则直接返回这个代理对象
- 如果还没有被代理过,则进行代理操作
- 通过去访问对象的
__v_isReactive属性,用来标识这个对象是否已经被代理过- 如果是一个没有被代理过的对象,那么这个属性应该是
undefined - 如果是一个已经被代理过的对象,那个去访问这个对象的属性,会触发
get操作,get操作中如果发现有来访问__v_isReactive属性,则直接返回true
- 如果是一个没有被代理过的对象,那么这个属性应该是
实现:
import { isObject } from '../utils'
const IS_REACTIVE_KEY = '__v_isReactive'
/** 缓存代理过的对象 */
const reactiveMap = new WeakMap()
const mutableHandlers = {
get(target, key, receiver) {
// 访问对象是否被代理过的属性
if (key === IS_REACTIVE_KEY) return true
const value = target[key]
console.log(`读取${key}属性, 属性值为${value}`)
return value
},
set(target, key, value, receiver) {
console.log(`将${key}属性值设置为${value}`)
return target[key] = value
}
}
/** 实现一个reactive函数,用于创建响应式对象 */
export function reactive(obj) {
// 首先判断参数是不是对象
if (!isObject(obj)) {
return obj
}
// 判断对象是否已经被代理过
if (obj[IS_REACTIVE_KEY]) {
return obj
}
// 如果代理过,则直接返回代理过的对象
if (reactiveMap.has(obj)) {
return reactiveMap.get(obj)
}
const proxy = new Proxy(obj, mutableHandlers)
// 缓存代理过的对象
reactiveMap.set(obj, proxy)
return proxy
}控制台输出: 
至此,解决了对象被代理之后,再次去代理它的代理对象的问题。
为什么不用 Symbol 作为是否响应式的标识
在上面的实现中,我们使用了一个字符串常量:
const IS_REACTIVE_KEY = '__v_isReactive'并通过访问该属性来判断一个对象是否已经被代理过。那么问题来了:
为什么不用 Symbol,而是用字符串?
从直觉上看,Symbol 具有“唯一性”“不易冲突”等特性,似乎更适合做内部标记。但在响应式系统中,使用字符串反而是更合理、更工程化的选择。
下面从几个关键角度来说明原因。
1. Symbol 在不同模块中无法可靠判断同一个标记
即使两个 Symbol 的描述是一样的,它们也不是同一个值:
Symbol('isReactive') === Symbol('isReactive') // false这意味着:
- 不同模块
- 不同文件
- 不同包
如果各自定义了一个 Symbol('isReactive'),它们永远无法互相识别。
而响应式系统往往是跨模块协作的,需要一个全局语义一致的标识。
使用字符串则不存在这个问题:
obj['__v_isReactive'] // 任何地方都能识别2. 响应式系统需要“可被访问”的标识
在当前实现中,有这样一段逻辑:
if (obj[IS_REACTIVE_KEY]) {
return obj
}这一步的目的不是“防止用户访问”,而是:
让系统内部可以快速判断:这个对象是不是一个 reactive 代理
使用字符串的好处是:
- 可以直接访问
- 可以在调试时看到
- 可以被工具函数(如 isReactive)复用
例如:
console.log(state.__v_isReactive) // true这对调试和教学都非常友好。
而 Symbol:
- 默认不可见
- 调试成本更高
- 不利于文档讲解和工具生态
3. Proxy 场景下,字符串比 Symbol 更可控
虽然 Proxy 的 get 可以拦截 Symbol,但在真实工程中会带来复杂性:
get(target, key) {
// key 可能是字符串
// 也可能是 Symbol.iterator、Symbol.toStringTag 等
}这会导致:
- 需要额外判断内置 Symbol
- 容易误伤逻辑
- 增加心智负担
而字符串键名:
if (key === '__v_isReactive') { ... }语义明确、逻辑清晰。
4. 字符串是“约定俗成”的内部标记方式
在 Vue 3 内部,也使用了类似的字符串标记方式:
__v_isReactive
__v_isReadonly
__v_raw这种以 _v 开头的命名本身就表明:
- 这是内部属性
- 不建议用户直接修改
- 冲突概率极低
相比之下,Symbol 并不会让系统“更安全”,只会让实现更复杂。
5. WeakMap + 字符串标记,解决的是两个不同问题
在当前实现中,我们同时使用了:
const reactiveMap = new WeakMap()
const IS_REACTIVE_KEY = '__v_isReactive'它们的职责是不同的:
- WeakMap:用来解决「同一个原始对象被多次代理」
- __v_isReactive:用来解决「代理对象再次被代理」
这也是为什么 WeakMap 并不能完全替代这个字符串标记。
6. 那能不能用 Symbol.for?
理论上可以:
const IS_REACTIVE = Symbol.for('v_isReactive')Symbol.for 是全局共享的 Symbol,可以解决模块间不一致的问题。
但在实践中:
- 可读性更差
- 调试不友好
- 没有明显收益
因此 Vue 以及大多数响应式实现,都选择了字符串方案。
通过 Reflect 来操作代理对象
在当前的实现中,直接使用了 target[key] 来读取代理对象的属性
看似是没有问题,但是这里会有坑
例:
const man = {
name: '小李大人',
get aliasName() {
return this.name + ' 的别名'
}
}
console.log(man.aliasName)控制台输出: 
这里是正常的,没有问题,但是当man这个对象被代理后就会有问题出现了
const man = {
name: '小李大人',
get aliasName() {
return this.name + ' 的别名'
}
}
const proxyMan = new Proxy(man, {
get(target, key, receiver) {
console.log('get', key)
return target[key]
}
})
console.log(proxyMan.aliasName)控制台输出: 
从这里的打印结果可以看到,这次读取aliasName属性时,get方法被调用了1次
但是aliasName是读取了name属性的,这也就表示着当前的操作中,name属性是没有被代理的,那么也就意味着在读取aliasName属性进行依赖搜集的时候,name属性的变化是不会被监听到的
我们应该是希望aliasName属性被读取时,name属性也被读取,这样name属性变化的时候,aliasName属性也能被读取到,从而触发响应式
那么我们就可以使用Reflect来操作代理对象,这样就可以保证name属性也会被读取到
const man = {
name: '小李大人',
get aliasName() {
return this.name + ' 的别名'
}
}
const proxyMan = new Proxy(man, {
get(target, key, receiver) {
console.log('get', key)
return Reflect.get(target, key, receiver)
}
})
console.log(proxyMan.aliasName)控制台输出: 
这样操作后,name属性也被读取到了,这样在进行依赖搜集的时候,name属性的变化也能被监听到
所以在实现reactive的时候也应该使用Reflect来操作代理对象,这样才能保证reactive对象的属性变化能被监听到
import { isObject } from '../utils'
const IS_REACTIVE_KEY = '__v_isReactive'
/** 缓存代理过的对象 */
const reactiveMap = new WeakMap()
const mutableHandlers = {
get(target, key, receiver) {
// 访问对象是否被代理过的属性
if (key === IS_REACTIVE_KEY) return true
const value = Reflect.get(target, key, receiver)
// 依赖收集 todo...
return value
},
set(target, key, value, receiver) {
// 触发更新 todo...
return Reflect.set(target, key, value, receiver)
}
}
/** 实现一个reactive函数,用于创建响应式对象 */
export function reactive(obj) {
// 首先判断参数是不是对象
if (!isObject(obj)) {
return obj
}
// 判断对象是否已经被代理过
if (obj[IS_REACTIVE_KEY]) {
return obj
}
// 如果代理过,则直接返回代理过的对象
if (reactiveMap.has(obj)) {
return reactiveMap.get(obj)
}
const proxy = new Proxy(obj, mutableHandlers)
// 缓存代理过的对象
reactiveMap.set(obj, proxy)
return proxy
}