Skip to content
medium
Proxy依赖收集

实现一个简化版的 Vue 响应式系统

题目描述

在 Vue 中,响应式系统是其最核心的特性之一。Vue 通过追踪数据的依赖,并在数据变更时通知更新,实现了组件的自动刷新。 请你模拟实现 Vue 的响应式系统中的一个简化版本 —— reactive 和 effect

具体要求如下:

  1. 实现一个函数 reactive(obj),将一个普通的对象变成响应式对象
  2. 实现一个函数 effect(fn),接收一个回调函数 fn,当响应式对象中的属性发生改变时,自动重新执行 fn。

题目模版

js
/**
 * 创建响应式对象
 * @param {object} obj - 需要变成响应式的对象
 * @returns {Proxy} 响应式对象
 */
function reactive(obj) {
  // 实现内容
}

/**
 * 注册副作用函数,当响应式数据变化时触发它
 * @param {Function} fn - 依赖响应式数据的函数
 */
function effect(fn) {
  // 实现内容
}
ts
import type { EffectFunc, ReactiveFunc } from './types'

export const effect: EffectFunc = (fn) => {}

export const reactive: ReactiveFunc = (obj) => {}
ts
// 副作用函数类型
export type EffectFunction = () => void

// effect函数接口
export interface EffectFunc {
  (fn: EffectFunction): void
}

// reactive函数接口
export interface ReactiveFunc {
  <T extends Record<string, unknown>>(obj: T): T
}

示例

javascript
const user = reactive({ name: 'Tom', age: 20 })

effect(() => {
  console.log('Name is', user.name)
})

user.name = 'Jerry'
// 输出:Name is Jerry

提示

使用 Proxy 对对象进行代理,拦截 get 和 set。

在 get 时记录依赖(即当前 effect 函数)。

在 set 时触发依赖(即重新运行相关 effect 函数)。

可以使用一个 Map(依赖桶)来存储对象属性和对应的 effect。

进阶

  • 支持嵌套对象的响应式(state.nested.value = 1)
  • 支持清除副作用(例如 stop(effectFn))
  • 支持多个 effect 并且能独立追踪不同属性

测试代码

js
import { describe, expect, it } from 'vitest'
import { effect, reactive } from './reactive_vue'

describe('vue 简化响应式系统测试', () => {
  it('应在副作用中访问初始值', () => {
    const obj = reactive({ count: 0 })
    let dummy
    effect(() => {
      dummy = obj.count
    })
    expect(dummy).toBe(0)
  })

  it('数据变化应重新触发 effect 执行', () => {
    const obj = reactive({ count: 1 })
    let dummy
    effect(() => {
      dummy = obj.count
    })
    obj.count = 10
    expect(dummy).toBe(10)
  })

  it('多个属性应独立触发各自的 effect', () => {
    const obj = reactive({ a: 1, b: 2 })
    let dummyA, dummyB
    effect(() => {
      dummyA = obj.a
    })
    effect(() => {
      dummyB = obj.b
    })
    obj.a = 100
    expect(dummyA).toBe(100)
    expect(dummyB).toBe(2)
  })

  it('修改无关属性不应影响 effect', () => {
    const obj = reactive({ foo: 1, bar: 2 })
    let dummy = 0
    effect(() => {
      dummy = obj.foo
    })
    obj.bar++ // 改 bar 不应触发 dummy 更新
    expect(dummy).toBe(1)
  })

  it('应支持多次修改后仍保持响应', () => {
    const obj = reactive({ n: 0 })
    const log = []
    effect(() => {
      log.push(obj.n)
    })
    obj.n = 1
    obj.n = 2
    expect(log).toEqual([0, 1, 2])
  })
})
ts
import { describe, expect, it } from 'vitest'
import { effect, reactive } from './reactive_vue'

describe('vue 简化响应式系统测试', () => {
  it('应在副作用中访问初始值', () => {
    const obj = reactive({ count: 0 })
    let dummy
    effect(() => {
      dummy = obj.count
    })
    expect(dummy).toBe(0)
  })

  it('数据变化应重新触发 effect 执行', () => {
    const obj = reactive({ count: 1 })
    let dummy
    effect(() => {
      dummy = obj.count
    })
    obj.count = 10
    expect(dummy).toBe(10)
  })

  it('多个属性应独立触发各自的 effect', () => {
    const obj = reactive({ a: 1, b: 2 })
    let dummyA, dummyB
    effect(() => {
      dummyA = obj.a
    })
    effect(() => {
      dummyB = obj.b
    })
    obj.a = 100
    expect(dummyA).toBe(100)
    expect(dummyB).toBe(2)
  })

  it('修改无关属性不应影响 effect', () => {
    const obj = reactive({ foo: 1, bar: 2 })
    let dummy = 0
    effect(() => {
      dummy = obj.foo
    })
    obj.bar++ // 改 bar 不应触发 dummy 更新
    expect(dummy).toBe(1)
  })

  it('应支持多次修改后仍保持响应', () => {
    const obj = reactive({ n: 0 })
    const log: number[] = []
    effect(() => {
      log.push(obj.n)
    })
    obj.n = 1
    obj.n = 2
    expect(log).toEqual([0, 1, 2])
  })
})

答案

类型路径
JS 版本problems/Day 03/answer.js
TS 版本problems/Day 03/ts/answer.ts
Review03.md

内容基于 MIT 许可 | 保持节奏 · 持续积累