第三章节 响应式的reactive与effect原理分析-【手摸手带你实现一个vue3】

37 阅读6分钟

大家好,我是作曲家种太阳,本次的专栏会带你一步步实现一个mini-vue3,每个小节都都回有一些测试,验证当前的一个逻辑,并且我已经把代码上传到github上了,可以根据每个章节去看对应的源码提交记录。

本章介绍循序渐进的介绍vue3的响应式系统,reactivity和effect都做了什么事情,没有需要写的代码,预计40分钟看完

前言

reactive 负责创建响应式数据对象,effect 则用来注册依赖于这些响应式数据的副作用函数(effect函数),两者共同实现响应式。

🌟 更通俗的解释: reactive:

用于把普通 JavaScript 对象转成具备响应式能力的对象(即 Proxy 对象)。

数据变化时,会自动触发 Proxy 的 getter 和 setter,方便追踪依赖 和 触发更新。

effect:

用于注册一个副作用函数(例如更新 DOM 视图的函数)。

当这个函数运行时,它所依赖的数据被访问,就会记录依赖关系(track)。

🚀 一.reactive 整体执行逻辑

  • 调用reactive() → 进入createReactiveObject()
  • 内部执行new Proxy(target, mutableHandlers)创建代理
  • 存入缓存proxyMap
  • 返回创建的proxy

步骤一:进入 createReactiveObject 方法

// 调用reactive
const proxy = reactive(target)

// reactive
function reactive(target) {
  return createReactiveObject(target, mutableHandlers)
}
  • 内部调用createReactiveObject方法。
  • 第二个参数是mutableHandlers,即代理对象的 handler 配置

步骤二:执行 Proxy 构造函数



// reactive 的构造主逻辑函数
function createReactiveObject(target, baseHandlers) {
  const proxy = new Proxy(target, baseHandlers)

  proxyMap.set(target, proxy)
  return proxy
}
  • proxyMap是一个WeakMap,用来缓存target和生成的proxy对象。
  • 作用:避免重复对同一对象创建多个代理。

此处关键:

  • 第一个参数:原始对象 target
  • 第二个参数:baseHandlers(实际传入的是mutableHandlers
  • mutableHandlers 来自源码文件:packages/reactivity/src/baseHandlers.ts

二. 📌 mutableHandlers 核心代码示例:

// mutableHandlers 内部结构 (baseHandlers.ts)
export const mutableHandlers = {
  get: createGetter(),
  set: createSetter()
}

// getter
function createGetter() {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    track(target, key) // 收集依赖
    return res
  }
}

// setter
function createSetter() {
  return function set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    trigger(target, key) // 触发依赖更新
    return result
  }
}

⚠️ 注意:此时不会触发 getter 和 setter(因为还没有读取或修改属性)。

三. effect 源码流程深度解析

Vue3 的响应式系统中,effect 起到了关键作用,它是注册副作用函数(effect 函数)的入口。

我们以源码路径 packages/reactivity/src/effect.ts 为例,追踪一下 effect 的完整运行逻辑。


Effect 流程

effect(fn)
    │
ReactiveEffect实例化
    │
run()首次执行
    │
副作用函数(fn)执行
    │
触发getter → 依赖收集 track()
    │
依赖关系存储到 targetMap

数据更新
    │
setter触发 → trigger()
    │
根据targetMap取出副作用函数
    │
重新执行副作用函数 → 视图自动更新

🚩 1、调用 effect 函数时发生了什么?

effect(() => {
  document.querySelector('#app').innerText = obj.name
})

① 调用 effect:

function effect(fn) {
  const _effect = new ReactiveEffect(fn)
  _effect.run()
}
  • 创建 ReactiveEffect 实例,拥有 run()stop() 方法
  • 执行 _effect.run() 会立即运行传入的副作用函数

② ReactiveEffect 类内部实现:

class ReactiveEffect {
  constructor(fn) {
    this.fn = fn
    this.deps = []
  }

  run() {
    activeEffect = this
    return this.fn()
  }

  stop() {
    // 停止响应式追踪
  }
}
  • run() 中会设置当前活跃的 effect,并执行副作用函数 fn

image.png


🚩 2、effect 函数中执行副作用时发生了什么?

③ 执行 fn 时触发 getter 与依赖收集:

get(target, key, receiver) {
  const res = Reflect.get(target, key, receiver)
  track(target, key)
  return res
}

④ track 依赖收集核心逻辑:

/**
 * 响应式依赖收集函数 —— 在读取响应式数据(getter)时调用
 * 用于将当前活跃的副作用函数(effect)收集到对应的依赖图中
 * @param target 被读取的响应式对象
 * @param key 被读取的属性名
 */
function track(target, key) {
  // 如果当前没有正在执行的副作用函数,直接返回
  // 即:不是在 effect(fn) 的执行过程中
  if (!activeEffect) return  // `activeEffect` 是一个全局变量

  // 获取当前 target 的依赖映射表(depsMap)
  // targetMap 是一个 WeakMap:target 对象 => Map(属性 => Set(effect))
  let depsMap = targetMap.get(target)

  // 如果没有 depsMap,就初始化一个并存储
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  // 获取当前属性的依赖集合(Set)
  let dep = depsMap.get(key)

  // 如果当前属性还没有被追踪过,创建一个新的 Set 存放 effect
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }

  // 把当前正在运行的 effect 函数添加到依赖集合中
  dep.add(activeEffect)

  // 同时反向记录:当前的 effect 也要保存它依赖了哪个 dep(用于后续清除依赖)
  activeEffect.deps.push(dep)
}

🔍 收集的结构如下:

targetMap = {
  target对象: {
    属性key: Set([effectFn1, effectFn2])
  }
}

🚩 3、数据变化触发更新的流程(setter → trigger)

setTimeout(() => {
  obj.name = '李四'
}, 2000)

① setter 被触发:

set(target, key, value, receiver) {
  const oldValue = target[key]
  const result = Reflect.set(target, key, value, receiver)
  trigger(target, key)  // 触发依赖
  return result
}

② trigger 执行:

/**
 * 响应式数据发生变化时调用,用于触发依赖于该数据的副作用函数(effect)
 * @param target 被修改的响应式对象
 * @param key 修改的属性名
 */
function trigger(target, key) {
  // 从全局依赖映射表中获取当前对象的依赖映射(depsMap)
  const depsMap = targetMap.get(target)
  if (!depsMap) return // 没有依赖说明这个对象没被响应式追踪过,直接返回

  // 获取当前属性 key 对应的所有 effect 依赖(一个 Set)
  const dep = depsMap.get(key)
  if (!dep) return // 没有依赖这个属性的 effect,返回

  // 触发所有依赖的副作用函数
  triggerEffects(dep)
}

/**
 * 依次执行所有依赖当前属性的副作用函数
 * @param dep Set<effect>,表示一组副作用函数集合
 */
function triggerEffects(dep) {
  // 遍历 Set 中的每一个 effect 实例,执行其 run 方法
  dep.forEach(effect => {
    effect.run() // 再次执行 effect 函数,完成响应式更新(例如更新 DOM)
  })
}

  • 找到之前收集过的副作用函数并重新执行,实现视图自动更新

targetMap 的数据结构

targetMap 是一个 嵌套结构的 WeakMap,用于记录每一个响应式对象的每一个属性,它们分别被哪些副作用函数(effect)所依赖,但是他到底是什么样的key与value形式呢

targetMap = WeakMap {
  obj => Map {
    'name' => Set { effect1, effect2 },
    'age'  => Set { effect1 }
  }
}
// 模拟 effect 函数
function effect1() { console.log('effect1 执行') }
function effect2() { console.log('effect2 执行') }

// 目标对象(响应式对象)
const obj = { name: '张三', age: 18 }

// 全局依赖表
const targetMap = new WeakMap()

// 模拟 track 函数 —— 收集依赖
function track(target, key, effectFn) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }

  dep.add(effectFn)
}

// 模拟 trigger 函数 —— 触发依赖
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

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

  dep.forEach(effectFn => effectFn())
}

// 模拟依赖收集过程
track(obj, 'name', effect1)
track(obj, 'name', effect2)
track(obj, 'age', effect1)

console.log('触发 name:')
trigger(obj, 'name') // effect1, effect2 执行

console.log('触发 age:')
trigger(obj, 'age')  // effect1 执行


🔐 为什么用 WeakMap 而不是 Map?

使用 WeakMap 的理由 说明
✅ 自动垃圾回收 target 对象被销毁后,自动释放内存
✅ 不会导致内存泄漏 Map 会强引用 key,WeakMap 不会
✅ target 一定是对象 响应式系统的目标对象都是对象或数组

总结

这一节我们学习了 reactivite 还有 Effect 的作用,说明了targetMap的数据结构 下一章节,我们就开始去手写这部分代码了