实现一个简化版的 Vue 响应式系统
题目描述
在 Vue 中,响应式系统是其最核心的特性之一。Vue 通过追踪数据的依赖,并在数据变更时通知更新,实现了组件的自动刷新。 请你模拟实现 Vue 的响应式系统中的一个简化版本 —— reactive 和 effect
具体要求如下:
- 实现一个函数 reactive(obj),将一个普通的对象变成响应式对象
- 实现一个函数 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 |
| Review | 03.md |