Skip to content

Vue 响应式系统

题目描述

实现一个简化版的 Vue 响应式系统,包含:

  • reactive(obj): 将普通对象变成响应式对象
  • effect(fn): 注册副作用函数,当响应式数据变化时自动执行

本题考查 响应式编程Proxy 代理依赖追踪 的核心原理。

核心知识点

1. 响应式系统核心原理

  • 依赖收集 (Dependency Collection): 在访问属性时记录哪些函数依赖了这个属性
  • 派发更新 (Trigger Update): 在修改属性时通知所有依赖该属性的函数重新执行
  • 自动执行: 数据变化自动触发相关计算和视图更新

2. 设计模式应用

  • 观察者模式: 数据是被观察者,effect 函数是观察者
  • 代理模式: Proxy 代理对象的属性访问和修改
  • 发布订阅模式: 属性变化时发布事件,订阅者自动执行

3. 数据结构设计

WeakMap {
  target1: Map {        // 对象级别
    key1: Set { effect1, effect2 },  // 属性级别 -> 副作用函数集合
    key2: Set { effect3 }
  },
  target2: Map {
    key1: Set { effect1 }
  }
}

代码实现

javascript
// 当前正在执行的副作用函数(用来依赖收集)
let activeEffect = null

// 依赖收集桶 - 三层数据结构
const bucket = new WeakMap()

// 注册副作用函数
export function effect(fn) {
  const effectFn = () => {
    // 清理函数:避免遗留的依赖关系
    cleanup(effectFn)
    // 设置当前活跃的副作用函数
    activeEffect = effectFn
    // 执行用户函数,触发依赖收集
    fn()
  }

  // 用于存储该副作用函数的依赖集合
  effectFn.deps = []

  // 立即执行一次
  effectFn()
}

// 清理函数:移除副作用函数的所有依赖
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}

// 依赖收集函数
function track(target, key) {
  if (!activeEffect)
    return // 没有活跃的副作用函数

  // 获取 target 对应的 Map,没有就创建
  let depsMap = bucket.get(target)
  if (!depsMap) {
    depsMap = new Map()
    bucket.set(target, depsMap)
  }

  // 获取 key 对应的 Set,没有就创建
  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }

  // 把当前副作用函数加入依赖集合
  deps.add(activeEffect)

  // 同时将依赖集合添加到副作用函数的 deps 数组中
  activeEffect.deps.push(deps)
}

// 派发更新函数
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap)
    return

  const effects = depsMap.get(key)
  if (!effects)
    return

  // 新建一个 Set 来避免无限循环
  const effectsToRun = new Set(effects)
  effectsToRun.forEach(effectFn => effectFn())
}

// 创建响应式对象
export function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key)

      // 返回实际值,使用 Reflect 确保正确的 this 绑定
      return Reflect.get(target, key, receiver)
    },

    set(target, key, value, receiver) {
      // 设置属性值
      const result = Reflect.set(target, key, value, receiver)

      // 派发更新
      trigger(target, key)

      return result
    },
  })
}

关键技术点

1. activeEffect 的作用机制

javascript
// 全局变量,用于标记当前正在执行的副作用函数
const activeEffect = null

effect(() => {
  // 执行时 activeEffect 指向这个函数
  console.log(user.name) // 访问 user.name 时建立依赖关系
})
// 执行完毕 activeEffect 重置为 null

2. WeakMap 的选择原因

javascript
// 使用 WeakMap 而不是 Map 的原因:
// 1. 键必须是对象,符合我们的需求
// 2. 弱引用,当对象被垃圾回收时,相关依赖也会自动清除
// 3. 避免内存泄漏

const bucket = new WeakMap()
// 当 obj 被回收时,bucket 中对应的条目也会被自动清除

3. 三层数据结构的必要性

javascript
// 第一层:WeakMap<Object, Map> - 区分不同的响应式对象
// 第二层:Map<string, Set> - 区分对象的不同属性
// 第三层:Set<Function> - 存储依赖该属性的所有副作用函数

WeakMap {
  userObj: Map {
    'name': Set { effect1, effect2 },
    'age': Set { effect3 }
  },
  postObj: Map {
    'title': Set { effect1 }
  }
}

4. Proxy vs Object.defineProperty

javascript
// Proxy 的优势:
// 1. 可以拦截任意属性的访问,包括新增属性
// 2. 支持数组索引的拦截
// 3. 支持 13 种拦截操作
// 4. 返回新对象,不直接修改原对象

// Object.defineProperty 的局限:
// 1. 只能劫持已存在的属性
// 2. 无法监听数组长度变化
// 3. 需要递归遍历对象的所有属性

5. 清理机制的重要性

javascript
function cleanup(effectFn) {
  // 必须清理旧的依赖关系,避免内存泄漏
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn) // 从依赖集合中移除该函数
  }
  effectFn.deps.length = 0 // 清空函数的依赖列表
}

6. 常见陷阱和坑点

  • 无限循环: 在 effect 中既读取又修改同一个属性
  • 依赖遗留: 不清理旧依赖导致的内存泄漏
  • this 绑定: 忘记使用 Reflect 导致 this 指向错误
  • 嵌套 effect: 嵌套调用时 activeEffect 的覆盖问题

使用示例

javascript
// 基础响应式
const user = reactive({ name: 'Tom', age: 20 })

effect(() => {
  console.log('用户名:', user.name) // 建立 name 属性的依赖
})
// 立即输出:用户名: Tom

user.name = 'Jerry' // 触发更新
// 输出:用户名: Jerry

// 多属性依赖
const state = reactive({ count: 0, doubleCount: 0 })

effect(() => {
  state.doubleCount = state.count * 2 // 依赖 count,修改 doubleCount
})

effect(() => {
  console.log(`Count: ${state.count}, Double: ${state.doubleCount}`)
})

state.count = 5
// 输出:Count: 5, Double: 10

// 条件依赖
const data = reactive({ show: true, name: 'Vue', age: 6 })

effect(() => {
  console.log(data.show ? data.name : data.age)
})
// 初始输出:Vue (建立 show 和 name 的依赖)

data.age = 7 // 不会触发更新,因为当前不依赖 age
data.show = false // 触发更新,输出:7 (现在依赖 age 而不是 name)
data.name = 'React' // 不会触发更新,因为现在不依赖 name

// 对象嵌套
const nested = reactive({
  user: { name: 'Alice', profile: { age: 25 } },
})

effect(() => {
  console.log(nested.user.name) // 只对 user 属性建立依赖
})

nested.user = { name: 'Bob', profile: { age: 30 } } // 触发更新
nested.user.name = 'Carol' // 不会触发更新(需要深度响应式)

// 数组操作
const list = reactive(['a', 'b', 'c'])

effect(() => {
  console.log('列表长度:', list.length)
})

list.push('d') // 触发更新
list[0] = 'A' // 如果实现了数组响应式,也会触发更新

记忆要点

核心记忆点

  1. 依赖收集时机 - effect 执行时访问响应式属性
  2. 更新触发时机 - 响应式属性被修改时
  3. 三层数据结构 - 对象 → 属性 → 副作用函数集合
  4. activeEffect 机制 - 全局标记当前执行的副作用函数
  5. 清理机制 - 避免依赖泄漏和无限循环

执行流程记忆

javascript
// 注册阶段
effect(fn) → activeEffect = fn → fn() → 访问属性 → track() → 建立依赖

// 更新阶段
修改属性 → set 拦截 → trigger() → 查找依赖 → 执行所有依赖函数

扩展思考

1. 深度响应式实现

javascript
function reactive(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj
  }

  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key)
      const res = Reflect.get(target, key, receiver)

      // 深度响应式:如果访问的是对象,递归创建响应式
      if (typeof res === 'object' && res !== null) {
        return reactive(res)
      }

      return res
    },

    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key)
      return result
    },
  })
}

2. 计算属性实现

javascript
function computed(getter) {
  let value
  let dirty = true // 标记是否需要重新计算

  const effectFn = effect(() => {
    value = getter()
    dirty = false
  })

  return {
    get value() {
      if (dirty) {
        effectFn()
      }
      return value
    },
  }
}

// 使用示例
const state = reactive({ count: 1 })
const doubleCount = computed(() => state.count * 2)

console.log(doubleCount.value) // 2
state.count = 3
console.log(doubleCount.value) // 6

3. 异步更新和调度器

javascript
// 任务队列,避免同步更新造成的性能问题
const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false

function flushJob() {
  if (isFlushing)
    return
  isFlushing = true

  p.then(() => {
    jobQueue.forEach(job => job())
    jobQueue.clear()
    isFlushing = false
  })
}

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    fn()
  }

  effectFn.deps = []
  effectFn.options = options

  if (!options.lazy) {
    effectFn()
  }

  return effectFn
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap)
    return

  const effects = depsMap.get(key)
  if (!effects)
    return

  const effectsToRun = new Set(effects)
  effectsToRun.forEach((effectFn) => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    }
    else {
      effectFn()
    }
  })
}

// 使用调度器实现异步更新
effect(
  () => {
    console.log(state.count)
  },
  {
    scheduler(fn) {
      jobQueue.add(fn)
      flushJob()
    },
  },
)

4. 支持数组方法的响应式

javascript
const arrayInstrumentations = {};

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(
  (method) => {
    arrayInstrumentations[method] = function (...args) {
      // 暂停追踪,避免 length 属性的依赖收集
      shouldTrack = false

      // 调用原始方法
      const res = Array.prototype[method].apply(this, args)

      // 恢复追踪
      shouldTrack = true

      return res
    }
  },
)

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      // 如果是数组且访问的是重写的方法
      if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
      }

      track(target, key)
      return Reflect.get(target, key, receiver)
    },

    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)

      // 只有当值真正改变时才触发更新
      if (oldValue !== value) {
        trigger(target, key)
      }

      return result
    },
  })
}

5. ref 和 reactive 的区别

javascript
function ref(value) {
  const wrapper = {
    value,
  }

  // 定义一个不可枚举的属性,用于标识 ref
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true,
  })

  return reactive(wrapper)
}

function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key]
    },
    set value(val) {
      obj[key] = val
    },
  }

  Object.defineProperty(wrapper, '__v_isRef', {
    value: true,
  })

  return wrapper
}

function toRefs(obj) {
  const ret = {}
  for (const key in obj) {
    ret[key] = toRef(obj, key)
  }
  return ret
}

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