🚀 深入浅出 Vue 3 响应式系统:从零构建核心 Reativity

86 阅读7分钟

🚀 深入浅出 Vue 3 响应式系统:从零构建核心 Reativity

本文将基于 Vue 3 的设计思想,带您从零开始,逐步构建一个完整的 Proxy 响应式系统。我们将拆解您的源码,详细解析每个步骤背后的 底层问题解决方案,确保您不仅学会如何实现,更能理解其工作原理。


一、系统基石:初始化与状态管理

在开始拦截数据操作之前,我们必须建立响应式系统的核心环境和数据存储结构。

1. 核心状态(activeEffecteffectStack

这是处理副作用函数执行环境的关键。

let activeEffect = null   // 💥 谁在使用数据?当前正在运行的 effect 函数
const effectStack = []    // 用于处理 effect 嵌套的栈
const TRANCK_KEY = Symbol() // 专用于 for...in / Object.keys() 依赖收集的键

2. 依赖存储桶(bucket

所有依赖关系都存储在一个多层嵌套的结构中。

const bucket = new WeakMap() 
const triggerType = {
  SET: 'SET',     // 修改属性值 (key 已存在)
  ADD: 'ADD',     // 新增属性 (key 不存在)
  DELETE: 'DELETE', // 删除属性
}
  • WeakMap:作为顶层存储,以响应式对象作为键。它的优势在于,如果对象在外部被垃圾回收,bucket 中对应的依赖也会自动清除,有效避免了 内存泄漏

二、核心驱动:effect(副作用函数)与依赖清理

effect 函数是响应式逻辑的入口,它定义了**“要做什么”**。高效的依赖清理是确保系统正确性的基石。

1. 实现 effect 函数

它负责设置执行环境,确保在 fn() 运行时能正确进行依赖收集。

function effect(fn, options = {}) {
  function effectFn() {
    clean(effectFn) // 1. 清理旧依赖
    effectStack.push(effectFn) // 2. 入栈
    activeEffect = effectFn // 3. 设置当前活跃 effect
    const res = fn() // 4. 执行 fn,触发 get -> track 收集依赖
    effectStack.pop() // 5. 出栈
    activeEffect = effectStack[effectStack.length - 1] // 6. 恢复上一个 effect
    return res
  }
  
  effectFn.deps = [] // 反向存储依赖集合,用于 clean
  effectFn.options = options
  
  if (!options.lazy) {
    effectFn() // 默认立即执行一次
  }
  return effectFn
}

2. 解决动态依赖问题:clean

问题: 如果 effect 内部使用了 if 语句(分支切换),我们必须在下次执行时移除旧的分支依赖。否则,修改不再需要的属性也会导致视图更新。

解决方案:effectFn 重新执行前,先调用 clean,彻底移除它在所有依赖集合中的记录。

function clean(effectFn) {
  if (!effectFn || !effectFn.deps) return
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn) // 从 Set 集合中移除 effect
  }
  effectFn.deps.length = 0 // 清空 effectFn 自身的引用
}

三、构建桥梁:track(收集)与 trigger(派发)

这组函数是响应式系统的中枢神经,连接着数据访问和副作用函数的执行。

1. 依赖收集:track

当属性被 get 访问时调用。核心是将当前的 activeEffect 存储到 bucket 中。

function track(target, key) {
  if (!activeEffect) return 
  
  // 1. 获取 target 对应的 depsMap
  let depsMap = bucket.get(target)
  if (!depsMap) { /* 初始化 depsMap */ }
  
  // 2. 获取 key 对应的 deps (Set 集合)
  let deps = depsMap.get(key)
  if (!deps) { /* 初始化 deps */ }
  
  // 3. 收集 activeEffect (Set 自动去重)
  deps.add(activeEffect)
  // 4. 反向收集引用,供 clean 时使用
  activeEffect.deps.push(deps)
}

2. 派发更新:trigger

当属性被 setdelete 时调用。核心是找到所有相关的 effect 并执行它们。

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

  const deps = depsMap.get(key)
  const effects = new Set(deps) // ⚡️ 优化:拷贝 Set,避免遍历期间集合突变
  
  // 💥 迭代操作依赖的处理(在第五节详解)
  if ([triggerType.ADD, triggerType.DELETE].includes(type)) {
    effects.add(...(depsMap.get(TRANCK_KEY) || []))
  }

  effects &&
    effects.forEach((fn) => {
      // ⚡️ 优化:避免无限递归循环 (如 effect 内部修改自身依赖属性)
      if (fn !== activeEffect) { 
        if (fn.options.scheduler) {
          fn.options.scheduler(fn) // 使用调度器(如 nextTick)
        } else {
          fn()
        }
      }
    })
}

四、核心拦截:处理属性 getset

我们使用 Proxygetset 拦截器来介入对象的读写操作。

function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === 'raw') return target
      if (!isReadonly) {
        track(target, key) // 读:收集依赖
      }
      const result = Reflect.get(target, key, receiver)
      
      // 递归代理实现深响应式
      if (!isShallow && result !== null && typeof result === 'object') {
        return reactive(result) 
      }
      return result
    },

    set(target, key, value, receiver) {
      if (isReadonly) { /* 警告并返回 */ }
      
      // ⚡️ 优化:避免原型链继承属性时的重复触发
      if (receiver.raw !== target) return true

      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      
      // 检查值是否变化 (包含 NaN 处理)
      if (value !== oldValue && (value === value || oldValue === oldValue)) {
        // 判断是新增还是修改
        const type = target.hasOwnProperty(key) ? triggerType.SET : triggerType.ADD
        trigger(target, key, type) // 写:派发更新
      }
      return result
    },
    // ... 其他拦截器
  })
}

🎯 何时触发?(get/set

场景触发类型需重新运行的 Effect关键点
修改 现有属性SET依赖该 key 的所有 Effect最常见的更新场景。
新增 属性ADD依赖该 key 的 Effect + 依赖 TRANCK_KEY 的 Effect新增属性不仅影响该 key 的访问,也改变了对象的 结构

五、拦截迭代操作:infor...in

对于 Vue 响应式系统,我们不仅要拦截属性的读写,还要拦截判断属性是否存在和遍历属性的行为。

1. 拦截 in 操作符(has

目标: 使 if ('prop' in obj) 具有响应性。

// ... 在 createReactive 的 Proxy 选项中添加:
has(target, key, receiver) {
  track(target, key) // 依赖该 key 的存在性
  return Reflect.has(target, key, receiver)
}

2. 拦截 for...in 循环(ownKeys

目标: 使 for (let k in obj) 在对象结构变化时(属性增删)能重新运行。

// ... 在 createReactive 的 Proxy 选项中添加:
ownKeys(target) {
  // 不依赖于任何特定属性,而是依赖于对象的“键列表”
  track(target, TRANCK_KEY) 
  return Reflect.ownKeys(target)
}

3. 拦截属性删除(deleteProperty

目标: 处理 delete obj.key,并确保 TRANCK_KEY 依赖得到更新。

JavaScript

// ... 在 createReactive 的 Proxy 选项中添加:
deleteProperty(target, key) {
  if (isReadonly) return true 
  
  const hasKey = Object.prototype.hasOwnProperty.call(target, key)
  const result = Reflect.deleteProperty(target, key)
  
  if (result && hasKey) {
    trigger(target, key, triggerType.DELETE) // 💥 派发 DELETE 类型更新
  }
  return result
}

🎯 何时触发?(has/ownKeys/delete

操作触发类型需重新运行的 Effect关键点
新增 属性ADD影响 key 的 effect + 影响 TRANCK_KEY 的 effect改变了 in 的结果和 for...in 列表。
删除 属性DELETE影响 key 的 effect + 影响 TRANCK_KEY 的 effect改变了 in 的结果和 for...in 列表。

最终 API 导出

通过组合 isShallowisReadonly 参数,我们导出了 Vue 提供的所有响应式 API。

function reactive(obj) { return createReactive(obj, false, false) }
function shallowReactive(obj) { return createReactive(obj, true, false) }
function readonly(obj) { return createReactive(obj, false, true) }
function shallowReadonly(obj) { return createReactive(obj, true, true) }

export {
  reactive,
  effect,
  // ... 其他导出的 track, trigger, shallowReactive, etc.
}

这份文档清晰地展现了 Vue 3 响应式系统如何通过 Proxy 构建一个精确、高效且无副作用的 观察者模式,实现了我们对数据的完全控制。最终源码如下:

let activeEffect = null
const bucket = new WeakMap()
const effectStack = []
const TRANCK_KEY = Symbol() // for in ownKeys
const triggerType = {
  SET: 'SET',
  ADD: 'ADD',
  DELETE: 'DELETE',
}

function effect(fn, options = {}) {
  function effectFn() {
    clean(effectFn)
    effectStack.push(effectFn)
    activeEffect = effectFn
    const res = fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    return res
  }
  effectFn.deps = []
  effectFn.options = options
  if (options.lazy) {
    return effectFn
  }
  effectFn()
}

function clean(effectFn) {
  if (!effectFn || !effectFn.deps) return
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}

/**
 * obj
 *  1、get set
 *  2、delete
 *  3、in操作符
 *  4、for in 循环读取
 *  5、对象本体和原型链对象都是响应式变量,set的时候,不需要触发两次,虽然在trigger里也有用set兜底
 *  6、实现深响应式和浅响应式,深可读和浅可读
 */

/**
 *
 * @param {*} obj
 * @param {*} isShallow 是否浅代理
 * @returns
 */
function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === 'raw') {
        return target
      }
      // 非只读才收集依赖
      if (!isReadonly) {
        track(target, key)
      }
      const result = Reflect.get(target, key, receiver)
      if (!isShallow) {
        if (result !== null && typeof result === 'object') {
          return reactive(result, isShallow)
        }
      }
      return result
    },
    set(target, key, value, receiver) {
      // 只读给出警告提示
      if (isReadonly) {
        console.warn(`${key} is readonly`)
        return
      }
      // 避免原型链重复触发
      if (receiver.raw !== target) return
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (value !== oldValue && (value === value || oldValue === oldValue)) {
        const type = target[key] ? triggerType.SET : triggerType.ADD
        trigger(target, key, type)
      }
      return result
    },
    has(target, key, receiver) {
      track(target, key)
      return Reflect.has(target, key, receiver)
    },
    deleteProperty(target, key) {
      if (isReadonly) {
        console.warn(`${key} is readonly`)
        return
      }
      const hasKey = Object.prototype.hasOwnProperty.call(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (result && hasKey) {
        trigger(target, key, triggerType.DELETE)
      }
      return result
    },
    ownKeys(target) {
      track(target, TRANCK_KEY)
      return Reflect.ownKeys(target)
    },
  })
}

function track(target, key) {
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    depsMap = new Map()
    bucket.set(target, depsMap)
  }
  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key, type) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const deps = depsMap.get(key)
  const effects = new Set(deps)
  if ([triggerType.ADD, triggerType.DELETE].includes(type)) {
    effects.add(...(depsMap.get(TRANCK_KEY) || []))
  }
  effects &&
    effects.forEach((fn) => {
      if (fn !== activeEffect) {
        if (fn.options.scheduler) {
          fn.options.scheduler(fn)
        } else {
          fn()
        }
      }
    })
}

/**
 * 创建响应式对象,深响应式且非可读
 * @param {*} obj
 * @returns
 */
function reactive(obj) {
  return createReactive(obj, false, false)
}

/**
 * 浅代理
 * @param {*} obj
 * @returns
 */
function shallowReactive(obj) {
  return createReactive(obj, true)
}

/**
 * 深可读
 * @param {*} obj
 * @returns
 */
function readonly(obj) {
  return createReactive(obj, false, true)
}

/**
 * 浅可读
 * @param {*} obj
 * @returns
 */
function shallowReadonly(obj) {
  return createReactive(obj, true)
}

export {
  reactive,
  effect,
  track,
  trigger,
  shallowReactive,
  readonly,
  shallowReadonly,
}