Skip to content

Vue 响应式原理 ​

响应式要解决的问题只有一句话:数据变了,怎么知道该更新哪块视图。 Vue2 和 Vue3 走了两条路:一个是「预先劫持每个属性」,一个是「访问时拦截整个对象」。

一、整体思路 ​

数据变化 → 找到「谁用了这份数据」 → 通知它重新执行

所以核心是两件事:
1. 收集依赖(谁用了)
2. 触发更新(通知谁)
数据变化 → 找到「谁用了这份数据」 → 通知它重新执行

所以核心是两件事:
1. 收集依赖(谁用了)
2. 触发更新(通知谁)

Vue 把「依赖」具体化成 effect(副作用函数):组件的渲染函数、watch 的回调、computed 的求值函数,都是 effect。

二、Vue2:Object.defineProperty ​

核心:把属性改成 getter/setter ​

Vue2 通过 Object.defineProperty 将 data 的属性转为 getter/setter,实现数据劫持。

js
const obj = { name: '张三', age: 18 }

Object.keys(obj).forEach(key => {
  let value = obj[key]

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 收集依赖:谁在读我,就把谁记下来
      dep.depend()
      return value
    },
    set(newValue) {
      if (newValue === value) return
      value = newValue
      // 触发更新:通知所有读过我的地方
      dep.notify()
    }
  })
})
const obj = { name: '张三', age: 18 }

Object.keys(obj).forEach(key => {
  let value = obj[key]

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 收集依赖:谁在读我,就把谁记下来
      dep.depend()
      return value
    },
    set(newValue) {
      if (newValue === value) return
      value = newValue
      // 触发更新:通知所有读过我的地方
      dep.notify()
    }
  })
})

关键点:Object.defineProperty 只能劫持已知的、已存在的属性,这是它一切限制的根源。

发布订阅模式 ​

Dep — 依赖收集器(每个属性一个) ​

js
class Dep {
  constructor() {
    this.subs = []        // 存放订阅者(Watcher)
  }
  depend() {
    if (Dep.target) this.addSub(Dep.target)
  }
  addSub(watcher) {
    if (!this.subs.includes(watcher)) this.subs.push(watcher)
  }
  notify() {
    this.subs.slice().forEach(watcher => watcher.update())
  }
}
Dep.target = null        // 全局「当前正在执行的 watcher」
class Dep {
  constructor() {
    this.subs = []        // 存放订阅者(Watcher)
  }
  depend() {
    if (Dep.target) this.addSub(Dep.target)
  }
  addSub(watcher) {
    if (!this.subs.includes(watcher)) this.subs.push(watcher)
  }
  notify() {
    this.subs.slice().forEach(watcher => watcher.update())
  }
}
Dep.target = null        // 全局「当前正在执行的 watcher」

Watcher — 订阅者(每个组件一个) ​

js
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.getter = typeof expOrFn === 'function'
      ? expOrFn
      : () => expOrFn.split('.').reduce((o, k) => o?.[k], vm)
    this.cb = cb
    this.value = this.get()
  }

  get() {
    Dep.target = this            // 标记:接下来读到的属性都算我依赖
    const value = this.getter()  // 触发 getter → 完成收集
    Dep.target = null
    return value
  }

  update() {
    queueWatcher(this)           // 不是立刻执行,先入队(批量更新)
  }

  run() {
    const newVal = this.get()
    if (newVal !== this.value) {
      const oldVal = this.value
      this.value = newVal
      this.cb.call(this.vm, newVal, oldVal)
    }
  }
}
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.getter = typeof expOrFn === 'function'
      ? expOrFn
      : () => expOrFn.split('.').reduce((o, k) => o?.[k], vm)
    this.cb = cb
    this.value = this.get()
  }

  get() {
    Dep.target = this            // 标记:接下来读到的属性都算我依赖
    const value = this.getter()  // 触发 getter → 完成收集
    Dep.target = null
    return value
  }

  update() {
    queueWatcher(this)           // 不是立刻执行,先入队(批量更新)
  }

  run() {
    const newVal = this.get()
    if (newVal !== this.value) {
      const oldVal = this.value
      this.value = newVal
      this.cb.call(this.vm, newVal, oldVal)
    }
  }
}

完整的数据劫持 ​

js
function observe(obj) {
  if (!obj || typeof obj !== 'object') return

  Object.keys(obj).forEach(key => {
    let value = obj[key]
    observe(value)              // 递归处理深层对象

    const dep = new Dep()

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        dep.depend()            // 收集:Dep.target 有值就记下
        return value
      },
      set(newValue) {
        if (newValue === value) return
        value = newValue
        observe(newValue)       // 新值也要劫持
        dep.notify()            // 发布通知
      }
    })
  })
}
function observe(obj) {
  if (!obj || typeof obj !== 'object') return

  Object.keys(obj).forEach(key => {
    let value = obj[key]
    observe(value)              // 递归处理深层对象

    const dep = new Dep()

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        dep.depend()            // 收集:Dep.target 有值就记下
        return value
      },
      set(newValue) {
        if (newValue === value) return
        value = newValue
        observe(newValue)       // 新值也要劫持
        dep.notify()            // 发布通知
      }
    })
  })
}

数据代理 ​

Vue 将 data 的属性代理到 vm 实例上,方便通过 this.xxx 直接访问。

js
class Vue {
  constructor(options) {
    this.$data = options.data
    observe(this.$data)

    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() { return this.$data[key] },
        set(v) { this.$data[key] = v }
      })
    })
  }
}
class Vue {
  constructor(options) {
    this.$data = options.data
    observe(this.$data)

    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() { return this.$data[key] },
        set(v) { this.$data[key] = v }
      })
    })
  }
}

数组的响应式:重写七个方法 ​

Object.defineProperty 无法监听数组索引和 length,所以 Vue2 直接重写数组原型上的七个变更方法:

js
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  const original = arrayProto[method]
  Object.defineProperty(arrayMethods, method, {
    value: function mutator(...args) {
      const result = original.apply(this, args)
      const ob = this.__ob__
      ob.dep.notify()                    // 通知数组的依赖
      return result
    }
  })
})
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  const original = arrayProto[method]
  Object.defineProperty(arrayMethods, method, {
    value: function mutator(...args) {
      const result = original.apply(this, args)
      const ob = this.__ob__
      ob.dep.notify()                    // 通知数组的依赖
      return result
    }
  })
})

所以 arr[0] = x 不响应式,而 arr.splice(0, 1, x) 响应式 —— 因为只有后者走了被重写的方法。

Vue2 的三个限制与 $set 的原理 ​

js
// 1. 新增/删除属性:defineProperty 没劫持过新 key
this.obj.newKey = 'x'                     // ❌ 非响应式
this.$set(this.obj, 'newKey', 'x')        // ✅
delete this.obj.key                       // ❌
this.$delete(this.obj, 'key')             // ✅

// 2. 通过索引改数组
this.list[0] = 'x'                        // ❌
this.$set(this.list, 0, 'x')              // ✅(内部转成 splice)

// 3. 直接改 length
this.list.length = 0                      // ❌
this.list.splice(0)                       // ✅
// 1. 新增/删除属性:defineProperty 没劫持过新 key
this.obj.newKey = 'x'                     // ❌ 非响应式
this.$set(this.obj, 'newKey', 'x')        // ✅
delete this.obj.key                       // ❌
this.$delete(this.obj, 'key')             // ✅

// 2. 通过索引改数组
this.list[0] = 'x'                        // ❌
this.$set(this.list, 0, 'x')              // ✅(内部转成 splice)

// 3. 直接改 length
this.list.length = 0                      // ❌
this.list.splice(0)                       // ✅

$set 做了三件事:给新属性补上 defineProperty 劫持 → 触发这个 key 的 dep → 触发对象本身的 ob.dep(因为 Vue.set 也可能作用于数组)。

三、Vue3:Proxy ​

核心:拦截整个对象 ​

js
const handler = {
  get(target, key, receiver) {
    track(target, key)                        // 收集依赖
    const res = Reflect.get(target, key, receiver)
    // 懒递归:只有访问到嵌套对象时才把它变成响应式
    return isObject(res) ? reactive(res) : res
  },
  set(target, key, value, receiver) {
    const oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver)
    if (!Object.is(oldValue, value)) trigger(target, key)   // 触发更新
    return result
  },
  deleteProperty(target, key) {
    const hadKey = Object.prototype.hasOwnProperty.call(target, key)
    const result = Reflect.deleteProperty(target, key)
    if (hadKey && result) trigger(target, key)
    return result
  },
  has(target, key) {
    track(target, key)
    return Reflect.has(target, key)
  }
}

const reactive = (obj) => new Proxy(obj, handler)
const handler = {
  get(target, key, receiver) {
    track(target, key)                        // 收集依赖
    const res = Reflect.get(target, key, receiver)
    // 懒递归:只有访问到嵌套对象时才把它变成响应式
    return isObject(res) ? reactive(res) : res
  },
  set(target, key, value, receiver) {
    const oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver)
    if (!Object.is(oldValue, value)) trigger(target, key)   // 触发更新
    return result
  },
  deleteProperty(target, key) {
    const hadKey = Object.prototype.hasOwnProperty.call(target, key)
    const result = Reflect.deleteProperty(target, key)
    if (hadKey && result) trigger(target, key)
    return result
  },
  has(target, key) {
    track(target, key)
    return Reflect.has(target, key)
  }
}

const reactive = (obj) => new Proxy(obj, handler)

Reflect 的作用:保证拦截器里对原对象的操作行为与默认一致,并把 receiver 正确透传(涉及原型链与访问器时的 this 指向)。

Vue3 的依赖结构(重点) ​

Vue2 是「每个属性一个 Dep 数组」,Vue3 是三层 Map:

js
// targetMap: WeakMap<target, depsMap>
// depsMap:   Map<key, dep>
// dep:       Set<effect>
const targetMap = new WeakMap()

let activeEffect = null

function track(target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) targetMap.set(target, (depsMap = new Map()))
  let dep = 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 dep = depsMap.get(key)
  if (dep) dep.forEach(effect => {
    if (effect !== activeEffect) effect.scheduler ? effect.scheduler() : effect.run()
  })
}
// targetMap: WeakMap<target, depsMap>
// depsMap:   Map<key, dep>
// dep:       Set<effect>
const targetMap = new WeakMap()

let activeEffect = null

function track(target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) targetMap.set(target, (depsMap = new Map()))
  let dep = 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 dep = depsMap.get(key)
  if (dep) dep.forEach(effect => {
    if (effect !== activeEffect) effect.scheduler ? effect.scheduler() : effect.run()
  })
}

为什么用 WeakMap:target 被回收时,依赖记录自动消失,不会内存泄漏。

effect 与调度器 ​

js
function effect(fn, scheduler) {
  const e = {
    run() {
      activeEffect = e
      const res = fn()          // 执行时读到的属性都会 track 到 e
      activeEffect = null
      return res
    },
    scheduler
  }
  e.run()
  return e
}
function effect(fn, scheduler) {
  const e = {
    run() {
      activeEffect = e
      const res = fn()          // 执行时读到的属性都会 track 到 e
      activeEffect = null
      return res
    },
    scheduler
  }
  e.run()
  return e
}

组件的渲染函数就是一个 effect。但它带 scheduler —— 数据变化时不是立刻重渲染,而是把任务塞进队列:

js
const queue = []
let flushing = false

function queueJob(job) {
  if (!queue.includes(job)) queue.push(job)
  queueFlush()
}

function queueFlush() {
  if (flushing) return
  flushing = true
  // 关键:用微任务批量刷新,同一轮里的多次修改只渲染一次
  Promise.resolve().then(flushJobs)
}

function flushJobs() {
  queue.sort((a, b) => a.id - b.id)   // 父组件先于子组件更新
  for (const job of queue) job()
  queue.length = 0
  flushing = false
}
const queue = []
let flushing = false

function queueJob(job) {
  if (!queue.includes(job)) queue.push(job)
  queueFlush()
}

function queueFlush() {
  if (flushing) return
  flushing = true
  // 关键:用微任务批量刷新,同一轮里的多次修改只渲染一次
  Promise.resolve().then(flushJobs)
}

function flushJobs() {
  queue.sort((a, b) => a.id - b.id)   // 父组件先于子组件更新
  for (const job of queue) job()
  queue.length = 0
  flushing = false
}

这就是 nextTick 的来源:你在一次事件里改了 10 个 ref,DOM 只在微任务刷新后更新一次。

四、ref / reactive / shallow 的区别 ​

API适用实现注意
reactive(obj)对象/数组Proxy解构会丢失响应式
ref(v)任意值(含基本类型)RefImpl 类,内部 dep要用 .value(模板里自动解包)
shallowRef / shallowReactive大对象、只关心整体替换只代理第一层深层变化不触发
toRef / toRefs从 reactive 取出单个属性返回带 getter 的对象解构后仍保持响应式
js
// ref 内部大致长这样
class RefImpl {
  constructor(value) {
    this._value = value
    this.dep = new Set()
  }
  get value() {
    track(this, 'value')     // 依赖记录在 ref 自己身上
    return this._value
  }
  set value(newVal) {
    if (Object.is(newVal, this._value)) return
    this._value = newVal
    trigger(this, 'value')
  }
}
// ref 内部大致长这样
class RefImpl {
  constructor(value) {
    this._value = value
    this.dep = new Set()
  }
  get value() {
    track(this, 'value')     // 依赖记录在 ref 自己身上
    return this._value
  }
  set value(newVal) {
    if (Object.is(newVal, this._value)) return
    this._value = newVal
    trigger(this, 'value')
  }
}

为什么 ref 要 .value:基本类型没法被 Proxy 代理,只能包一层对象;.value 的 getter/setter 就是劫持点。

五、computed 与 watch ​

computed:带缓存的 effect ​

js
function computed(getter) {
  let value
  let dirty = true                     // 脏标记:是否需要重算
  const effect = { run() { value = getter() }, scheduler: () => { dirty = true } }

  return {
    get value() {
      if (dirty) {                     // 只有依赖变了才重算
        dirty = false
        effect.run()
      }
      track({ computed }, 'value')     // 让使用方依赖这个 computed
      return value
    }
  }
}
function computed(getter) {
  let value
  let dirty = true                     // 脏标记:是否需要重算
  const effect = { run() { value = getter() }, scheduler: () => { dirty = true } }

  return {
    get value() {
      if (dirty) {                     // 只有依赖变了才重算
        dirty = false
        effect.run()
      }
      track({ computed }, 'value')     // 让使用方依赖这个 computed
      return value
    }
  }
}

缓存的关键就是 dirty:依赖没变就直接返回上次的值,不重新执行 getter。

watch vs watchEffect ​

watch(source, cb)watchEffect(fn)
依赖收集显式指定 source执行 fn 时自动收集
首次执行默认不执行(immediate: true 才执行)立即执行一次
能否拿到旧值能不能
适合需要在变化时做副作用(请求、打点)依赖不固定、只要跟着变

六、响应式丢失的三个场景 ​

1. 解构 reactive ​

js
const state = reactive({ count: 0 })
const { count } = state      // ❌ count 是普通数字,断了
const { count } = toRefs(state)  // ✅ 仍是 ref
const state = reactive({ count: 0 })
const { count } = state      // ❌ count 是普通数字,断了
const { count } = toRefs(state)  // ✅ 仍是 ref

原因:解构取的是「值」,不是「属性的 getter」。

2. 替换整个 reactive 对象 ​

js
let state = reactive({ a: 1 })
state = reactive({ a: 2 })   // ❌ 前面的引用失效(代理对象被换掉)
Object.assign(state, { a: 2 })  // ✅ 保持同一个代理
let state = reactive({ a: 1 })
state = reactive({ a: 2 })   // ❌ 前面的引用失效(代理对象被换掉)
Object.assign(state, { a: 2 })  // ✅ 保持同一个代理

3. 把响应式对象传进非响应式容器 ​

js
const raw = { nested: reactive({ x: 1 }) }   // raw 本身不是响应式
// 但 nested.x 变了仍会触发(因为 nested 是代理)
const raw = { nested: reactive({ x: 1 }) }   // raw 本身不是响应式
// 但 nested.x 变了仍会触发(因为 nested 是代理)

七、Vue2 vs Vue3 对比 ​

特性Object.defineProperty(Vue2)Proxy(Vue3)
监听方式遍历属性逐个劫持代理整个对象
新增/删除属性无法监听,需 $set/$delete自动监听
数组索引/length无法监听,需重写七个方法自动监听
嵌套对象初始化时递归(慢)访问时懒递归(快)
依赖结构属性 → Dep → Watcher 数组target → key → Set<effect>
更新方式队列 + nextTick队列 + nextTick(同一套思路)
兼容性IE9+不支持 IE

八、速查 ​

问题答案
Vue2 用什么实现响应式Object.defineProperty 的 getter/setter
Vue2 为什么监听不到新增属性新属性没被 defineProperty 劫持过,要 $set
Vue2 为什么监听不到 arr[0]=x没走被重写的七个数组方法
Vue3 用什么Proxy + Reflect
Vue3 依赖存哪WeakMap<target, Map<key, Set<effect>>>
数据变了为什么只渲染一次渲染 effect 带 scheduler,塞进微任务队列批量刷新
nextTick 是什么等 DOM 更新完成后的回调,本质是那个微任务队列
computed 为什么有缓存dirty 标记,依赖没变就不重算
ref 为什么要 .value基本类型无法被 Proxy 代理,包一层对象才有劫持点
解构 reactive 为什么丢响应式解构取到的是值,不是 getter;用 toRefs