Skip to content

手写 reactive

reactive 创建一个响应式的对象

js
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
        }
    })
}

示例

vue
<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,同一个对象会被多次代理,如下:

vue
<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是被重复代理了,通过加入缓存来解决重复代理同一个对象的问题:

通过加入缓存来解决重复代理同一个对象的问题:

js
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
}

示例

vue
<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>

控制台输出: 控制台输出

这里成功的解决了同一个对象被多次代理的问题

同一个对象被代理后,再去代理它的代理对象,也会被重复代理

上一节中解决了同一个对象被多次代理的问题,但是同一个对象被代理后,再去代理它的代理对象,也会被重复代理

示例

vue
<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>

控制台输出: 控制台输出

为了解决这个问题:

  1. 需要在代理对象之前,先判断这个对象是否已经被代理过
  2. 如果已经被代理过,则直接返回这个代理对象
  3. 如果还没有被代理过,则进行代理操作
  4. 通过去访问对象的__v_isReactive属性,用来标识这个对象是否已经被代理过
    • 如果是一个没有被代理过的对象,那么这个属性应该是undefined
    • 如果是一个已经被代理过的对象,那个去访问这个对象的属性,会触发get操作,get操作中如果发现有来访问__v_isReactive属性,则直接返回true

实现:

js
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 作为是否响应式的标识

在上面的实现中,我们使用了一个字符串常量:

js
const IS_REACTIVE_KEY = '__v_isReactive'

并通过访问该属性来判断一个对象是否已经被代理过。那么问题来了:

为什么不用 Symbol,而是用字符串?

从直觉上看,Symbol 具有“唯一性”“不易冲突”等特性,似乎更适合做内部标记。但在响应式系统中,使用字符串反而是更合理、更工程化的选择。

下面从几个关键角度来说明原因。

1. Symbol 在不同模块中无法可靠判断同一个标记

即使两个 Symbol 的描述是一样的,它们也不是同一个值:

js
Symbol('isReactive') === Symbol('isReactive') // false

这意味着:

  • 不同模块
  • 不同文件
  • 不同包

如果各自定义了一个 Symbol('isReactive'),它们永远无法互相识别。

而响应式系统往往是跨模块协作的,需要一个全局语义一致的标识。

使用字符串则不存在这个问题:

js
obj['__v_isReactive'] // 任何地方都能识别

2. 响应式系统需要“可被访问”的标识

在当前实现中,有这样一段逻辑:

js
if (obj[IS_REACTIVE_KEY]) {
    return obj
}

这一步的目的不是“防止用户访问”,而是:

让系统内部可以快速判断:这个对象是不是一个 reactive 代理

使用字符串的好处是:

  • 可以直接访问
  • 可以在调试时看到
  • 可以被工具函数(如 isReactive)复用

例如:

js
console.log(state.__v_isReactive) // true

这对调试和教学都非常友好。

而 Symbol:

  • 默认不可见
  • 调试成本更高
  • 不利于文档讲解和工具生态

3. Proxy 场景下,字符串比 Symbol 更可控

虽然 Proxy 的 get 可以拦截 Symbol,但在真实工程中会带来复杂性:

js
get(target, key) {
    // key 可能是字符串
    // 也可能是 Symbol.iterator、Symbol.toStringTag 等
}

这会导致:

  • 需要额外判断内置 Symbol
  • 容易误伤逻辑
  • 增加心智负担

而字符串键名:

js
if (key === '__v_isReactive') { ... }

语义明确、逻辑清晰。

4. 字符串是“约定俗成”的内部标记方式

在 Vue 3 内部,也使用了类似的字符串标记方式:

txt
__v_isReactive
__v_isReadonly
__v_raw

这种以 _v 开头的命名本身就表明:

  • 这是内部属性
  • 不建议用户直接修改
  • 冲突概率极低

相比之下,Symbol 并不会让系统“更安全”,只会让实现更复杂。

5. WeakMap + 字符串标记,解决的是两个不同问题

在当前实现中,我们同时使用了:

js
const reactiveMap = new WeakMap()
const IS_REACTIVE_KEY = '__v_isReactive'

它们的职责是不同的:

  • WeakMap:用来解决「同一个原始对象被多次代理」
  • __v_isReactive:用来解决「代理对象再次被代理」

这也是为什么 WeakMap 并不能完全替代这个字符串标记。

6. 那能不能用 Symbol.for?

理论上可以:

js
const IS_REACTIVE = Symbol.for('v_isReactive')

Symbol.for 是全局共享的 Symbol,可以解决模块间不一致的问题。

但在实践中:

  • 可读性更差
  • 调试不友好
  • 没有明显收益

因此 Vue 以及大多数响应式实现,都选择了字符串方案。

通过 Reflect 来操作代理对象

在当前的实现中,直接使用了 target[key] 来读取代理对象的属性

看似是没有问题,但是这里会有坑

例:

js
const man = {
    name: '小李大人',
    get aliasName() {
        return this.name + ' 的别名'
    }
}

console.log(man.aliasName)

控制台输出: 控制台输出

这里是正常的,没有问题,但是当man这个对象被代理后就会有问题出现了

js
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属性也会被读取到

js
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对象的属性变化能被监听到

js
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
}