Skip to content

Vue3 响应式原理最小实现

这篇文档不追求完整还原 Vue3 源码,而是聚焦面试里最常被问到的那条主线: Proxy 是如何劫持数据的,ref/reactive 有什么区别,以及 effect 是如何自动收集依赖和触发更新的。

一、核心要点速览

💡 核心考点

  • Proxy 替代 Object.defineProperty:Vue3 使用 Proxy 拦截整个对象操作,性能更好且能检测新增/删除属性。
  • ref vs reactiveref 通过 .value 访问,适合基本类型;reactive 直接返回代理对象,适合引用类型。
  • effect 是入口:组件渲染时会创建 effect,执行时访问响应式数据自动完成依赖收集。
  • track 负责收集:读取数据时调用 track(),把当前 effect 存入 targetMap。
  • trigger 负责通知:修改数据时调用 trigger(),从 targetMap 取出相关 effects 并执行。

二、整体流程图

下面这张图专门对应本文的"最小实现版"代码,用来串起从模板到视图更新的完整链路:

Vue3 响应式最小实现流程图

可以先记住这两条主线:

  • 初始化链路template -> render -> effect() -> track() -> 依赖收集
  • 更新链路setter/trigger -> trigger() -> effect 重新执行 -> render -> 更新视图

三、核心实现代码(精简版)

js
/* ================= 依赖管理 ================= */
const targetMap = new WeakMap() // 存储所有响应式对象的依赖关系
let activeEffect = null // 当前正在执行的 effect

// 收集依赖
function track(target, key) {
  if (!activeEffect) return
  
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  
  let dep sp = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  
  dep.add(activeEffect)
}

// 触发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  const dep5p = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}

/* ================= effect(副作用函数) ================= */
function effect(fn, options = {}) {
  const _effect = function(...args) {
    try {
      activeEffect = _effect
      return fn(...args)
    } finally {
      activeEffect = null
    }
  }
  
  _effect.options = options
  
  // 立即执行一次(除非标记为 lazy)
  if (!options.lazy) {
    _effect()
  }
  
  return _effect
}

/* ================= ref(基础类型响应式) ================= */
class RefImpl {
  constructor(rawValue) {
    this._rawValue = rawValue
    this._value = rawValue
    this.dep = new Set() // 存储依赖此 ref 的 effects
  }
  
  get value() {
    trackRefValue(this)
    return this._value
  }
  
  set value(newValue) {
    if (newValue !== this._rawValue) {
      this._rawValue = newValue
      this._value = newValue
      triggerRefValue(this)
    }
  }
}

function trackRefValue(ref) {
  if (activeEffect) {
    ref.dep.add(activeEffect)
  }
}

function triggerRefValue(ref) {
  ref.dep.forEach(effect => effect())
}

function ref(value) {
  return new RefImpl(value)
}

function isRef(r) {
  return r instanceof RefImpl
}

function unref(ref) {
  return isRef(ref) ? ref.value : ref
}

/* ================= reactive(对象响应式) ================= */
function reactive(target) {
  return createReactiveObject(target)
}

function createReactiveObject(target) {
  if (!isObject(target)) {
    return target
  }
  
  const handler = {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      
      // 如果是 ref,自动解包(除了作为数组索引访问时)
      if (isRef(res) && !Array.isArray(target)) {
        return res.value
      }
      
      // 收集依赖
      track(target, key)
      
      // 如果返回值是对象,递归代理
      return isObject(res) ? reactive(res) : res
    },
    
    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
    },
    
    deleteProperty(target, key) {
      const hadKey = key in target
      const result = Reflect.deleteProperty(target, key)
      
      if (hadKey) {
        trigger(target, key)
      }
      
      return result
    }
  }
  
  return new Proxy(target, handler)
}

function isObject(value) {
  return value !== null && typeof value === 'object'
}

/* ================= computed(计算属性) ================= */
class ComputedRefImpl {
  constructor(getter) {
    this._getter = getter
    this._value = undefined
    this._dirty = true
    this.effect = effect(getter.bind(this), { lazy: true })
  }
  
  get value() {
    if (this._dirty) {
      this._value = this.effect()
      this._dirty = false
    }
    
    // 收集对 computed 的依赖
    trackRefValue(this)
    
    return this._value
  }
  
  // 当依赖变化时,标记为脏
  _onTrigger() {
    this._dirty = true
  }
}

function computed(getterOrOptions) {
  let getter
  
  if (typeof getterOrOptions === 'function') {
    getter = getterOrOptions
  } else {
    getter = getterOrOptions.get
  }
  
  return new ComputedRefImpl(getter)
}

/* ================= watch(监听器) ================= */
function watch(source, cb, options = {}) {
  let getter
  
  // 处理不同的 source 类型
  if (isRef(source)) {
    getter = () => source.value
  } else if (isReactive(source)) {
    getter = () => traverse(source)
  } else if (typeof source === 'function') {
    getter = source
  }
  
  let oldValue, newValue
  
  const job = () => {
    newValue = effectRunner()
    
    if (cb) {
      cb(newValue, oldValue)
    }
    
    oldValue = newValue
  }
  
  const effectRunner = effect(getter, {
    lazy: true,
    scheduler: job // 使用调度器
  })
  
  // 立即执行一次
  if (!options.immediate) {
    oldValue = effectRunner()
  } else {
    job()
  }
  
  return () => {
    // 清理函数(简化版)
  }
}

function traverse(obj, seen = new Set()) {
  if (!isObject(obj) || seen.has(obj)) {
    return obj
  }
  
  seen.add(obj)
  
  for (const key in obj) {
    traverse(obj[key], seen)
  }
  
  return obj
}

function isReactive(obj) {
  return obj && obj.__v_isReactive
}

/* ================= Vue 组件(迷你版) ================= */
class Vue {
  constructor(options) {
    this.$data = reactive(options.data || {})
    this.$el = options.el
    
    // 代理 data 到实例上
    if (options.data) {
      Object.keys(options.data).forEach(key => {
        Object.defineProperty(this, key, {
          get: () => this.$data[key],
          set: v => (this.$data[key] = v)
        })
      })
    }
    
    // 处理 setup
    if (options.setup) {
      this._setupState = options.setup()
    }
    
    // 处理 computed
    if (options.computed) {
      this._initComputed(options.computed)
    }
    
    // 处理 watch
    if (options.watch) {
      this._initWatch(options.watch)
    }
    
    // 渲染 effect
    if (options.render) {
      this.$render = options.render
      effect(() => {
        const vnode = this.$render.call(this)
        this._update(vnode)
      })
    }
  }
  
  _update(vnode) {
    console.log('🖥️ 更新视图:', vnode)
  }
  
  _initComputed(computed) {
    Object.keys(computed).forEach(key => {
      const c = computed(key)
      Object.defineProperty(this, key, {
        get: () => c.value,
        enumerable: true
      })
    })
  }
  
  _initWatch(watch) {
    Object.keys(watch).forEach(key => {
      watch(key, (newVal, oldVal) => {
        console.log(`👀 watch ${key}:`, oldVal, '→', newVal)
      })
    })
  }
}

四、模板如何转成 Render

Vue3 同样不会直接执行模板字符串,而是先把模板编译成 render 函数。 不同的是,Vue3 的 render 函数会使用 _ctx 上下文来访问响应式数据。

1. 模板写法

html
<div>{{ count }}</div>

2. 等价的 Render 思想

js
function render() {
  return `<div>${this.count}</div>`
}

这里最关键的是 this.count。 因为一旦访问它,就会触发 Proxy 的 get 拦截,而 get 内部正是 track() 收集 effect 的入口。


五、运行示例

结合上面的最小实现代码,可以这样模拟一次完整的初始化与更新过程:

js
const vm = new Vue({
  data: {
    count: 0,
    price: 10
  },

  computed: {
    total() {
      return this.count * this.price
    }
  },

  watch: {
    count(newVal, oldVal) {
      console.log('👀 watch count:', oldVal, '→', newVal)
    }
  },

  render() {
    return `count: ${this.count}, total: ${this.total}`
  }
})




setTimeout(() => {
  vm.count++ 
  // 👉 watch 回调触发
  // 👉 computed total 重新计算
  // 👉 视图更新
}, 1000)

这个示例里,第一次会触发初始化渲染;1 秒后修改 count,就会走一遍完整的更新链路。


六、执行流程拆解

1. 初始化阶段:effect 是怎么被收集进去的?

text
new Vue()
-> effect(() => render())
-> activeEffect = effect
-> 执行 render()
-> 访问 this.count
-> 触发 Proxy.get
-> track(target, 'count')
-> 将 activeEffect 存入 targetMap
-> activeEffect = null
-> _update() 渲染视图

这一阶段的重点是:

  • 先有当前 effecteffect() 执行前,先把 activeEffect 指向自己。
  • 再访问响应式数据render() 内部读取 this.count,触发 Proxy 的 get 拦截。
  • 最后完成依赖收集get 内部调用 track(),把当前 effect 存入 targetMap

所以本质上不是"模板绑定 effect",而是"Render 取值时,Proxy 自动把当前 effect 收集进去"。


2. 更新阶段:数据修改后为什么能更新视图?

text
vm.count = 1
-> 触发 Proxy.set
-> trigger(target, 'count')
-> 从 targetMap 取出相关 effects
-> 执行每个 effect
-> 重新执行 render()
-> 再次触发 get
-> 重新生成 vnode
-> _update() 更新视图

这一阶段的重点是:

  • Proxy.set 负责广播变化:数据一改,trigger() 就会执行。
  • effect 负责重新渲染:收到通知后重新执行。
  • Render 再跑一遍:于是新的数据被读到,新的视图结果被计算出来。

七、角色分工

1. targetMap 是干什么的?

  • 它是一个 WeakMap,存储所有响应式对象的依赖关系。
  • 结构:targetMap -> target -> depsMap -> key -> dep(Set of effects)
  • 当属性变化时,它负责快速找到相关的 effects。

2. effect 是干什么的?

  • effect 是"执行者"。
  • 对于渲染场景,它负责重新执行 render,让视图拿到最新数据。
  • 对于计算属性、用户 watch 等场景,本质上也都是类似的观察者机制。

3. Proxy 做了什么?

  • 它通过 get/set/deleteProperty 等拦截器劫持整个对象的操作。
  • get 负责依赖收集(调用 track())。
  • set 负责派发更新(调用 trigger())。

八、Vue2 vs Vue3 响应式对比

特性Vue2 (Object.defineProperty)Vue3 (Proxy)
实现方式遍历对象属性,逐个定义 getter/setter直接代理整个对象
性能初始化时需要递归遍历所有属性,大对象慢懒代理,访问时才递归,性能好
新增属性❌ 无法检测,需用 Vue.set✅ 天然支持
删除属性❌ 无法检测,需用 Vue.delete✅ 天然支持
数组索引修改❌ 需要特殊处理✅ 天然支持
Map/Set 支持❌ 不支持✅ 原生支持
内存占用较高(每个属性都有 dep)较低(按需创建 dep)

九、ref vs reactive 对比

特性refreactive
适用类型所有类型(基础类型 + 引用类型)仅引用类型(对象、数组)
访问方式需要 .value直接访问属性
替换整个对象✅ 支持❌ 会丢失响应性
模板中使用自动解包,不需要 .value直接使用
内部实现通过类实例的 getter/setter通过 Proxy 代理
推荐场景基础类型、需要替换整个对象对象/数组的属性修改

💡 最佳实践

  • 优先使用 ref:统一心智模型,避免混淆。
  • reactive 适合固定结构的对象:如表单数据、配置项等。
  • 不要混用:避免在同一个组件中混合使用导致混乱。

十、面试高频回答

1. Vue3 为什么用 Proxy 替代 Object.defineProperty?

答: Proxy 有以下优势:

  1. 性能更好:不需要在初始化时递归遍历所有属性,而是访问时才代理(懒代理)。
  2. 功能更强:可以检测属性的新增和删除,支持 Map/Set 等数据结构。
  3. 数组支持完善:天然支持数组索引修改和 length 变化,无需特殊处理。
  4. 内存更省:按需创建依赖集合,而不是为每个属性都创建一个 dep。

2. ref 和 reactive 有什么区别?

答:

  • ref 通过 .value 访问,适合基础类型和需要替换整个对象的场景;内部通过类的 getter/setter 实现。
  • reactive 直接访问属性,适合固定结构的对象;内部通过 Proxy 实现。
  • 推荐优先使用 ref,保持统一的心智模型。

3. effect 是如何自动收集依赖的?

答: effect 执行前会把自身赋值给全局变量 activeEffect,然后执行传入的函数。函数内部访问响应式数据时会触发 Proxy 的 get 拦截,get 内部调用 track(),把 activeEffect 存入 targetMap。执行完成后将 activeEffect 置为 null。这样就完成了自动依赖收集。

4. 一句话概括整条链路?

答: effect 执行时收集依赖,数据修改时触发更新,effect 重新执行并更新视图。


十一、最终总结

text
初始化:
模板 -> render -> effect() -> Proxy.get -> track() -> 依赖收集 -> _update()

更新:
Proxy.set -> trigger() -> effect 重新执行 -> render -> _update()

最需要记住的一句话是:

Vue3 通过 Proxy 劫持整个对象,effect 执行时自动收集依赖,数据修改时自动触发更新,完全不需要像 Vue2 那样遍历对象属性。

最近更新