人人都会AI编程

4.1 Vue 2 响应式:Object.defineProperty 实现

更新时间:2026-07-09

Vue 最核心的特性是“数据变化,视图自动更新”,这在 Vue 2 中是借助 Object.defineProperty 实现的。理解这一机制,不仅能帮你避坑,也能让你体会到 Vue 3 用 Proxy 重写响应式系统的原因。

核心思想:数据劫持 + 依赖收集 + 派发更新

Object.defineProperty 允许我们为一个对象的属性定义 getset 拦截器。Vue 2 正是利用这种“劫持”能力,在读取属性时收集依赖(谁用了这个数据),在修改属性时通知依赖更新(让用到的地方重新渲染)。

当你在 Vue 中写下这些数据:

data() {
  return {
    name: 'Alice',
    age: 25
  }
}

内部会被递归地转换成这样:

Object.defineProperty(obj, 'name', {
  get() {
    // 收集依赖:记住当前正在渲染的组件(Watcher)
    dep.depend()
    return value
  },
  set(newVal) {
    if (newVal === value) return
    value = newVal
    // 通知更新:让所有依赖此属性的 Watcher 重新执行
    dep.notify()
  }
})

这里的 dep 是一个“依赖管理器”,它维护了一个订阅者列表。组件渲染时,执行渲染函数会读取响应式数据,触发 get,当前组件的“Watcher”就被收集进去。当数据修改触发 setdep.notify() 会通知所有 Watcher 重新渲染。

依赖收集全流程

  1. 每个组件实例都有一个对应的 Watcher(渲染 Watcher)。
  2. 组件首次渲染时,会触发 data 中属性的 get,此时会把组件的 Watcher 添加到该属性的依赖列表(Dep)中。
  3. 当属性值被修改时,set 中调用 dep.notify(),遍历这个列表,让所有订阅的 Watcher 重新执行更新视图。

这就构建了一套“谁用了我,我就告诉谁我变了”的机制。关键点:Vue 2 是在 get 中依赖收集,在 set 中触发更新,因此只有被 defineProperty 处理过的属性才具备响应式能力。

数组的响应式特殊处理

由于 Object.defineProperty 可以直接劫持对象属性,但却无法拦截数组下标的直接修改(arr[0] = 'x'),也无法拦截 .length 的变化。因此,Vue 2 对数组做了两件事:

  • 重写七种变更方法pushpopshiftunshiftsplicesortreverse。这些方法在执行原操作后,会手动触发依赖更新。
  • 不处理索引与 length:直接通过索引设置元素(arr[0] = newVal)或修改 length,不会触发更新。这在开发中极易踩坑。

同时,对数组中每个元素(如果是对象)仍然会进行响应式处理,所以 arr.push({ name: 'Bob' }) 后,新对象的属性依然可响应。

原生缺陷:新增/删除属性不触发更新、数组索引修改不生效

Object.defineProperty 只能劫持对象已有的属性。这会带来三个典型问题:

  1. 新增属性:给 data 中的对象动态添加一个全新的属性,obj.newProp = 'hello',Vue 2 无法检测。必须使用 Vue.set(obj, 'key', value) 或者 this.$set()
  2. 删除属性delete obj.prop 同样不会触发更新,需要使用 Vue.delete(obj, 'key')
  3. 数组索引直接赋值arr[0] = newItem 无效,需要用 Vue.set(arr, 0, newItem)arr.splice(0, 1, newItem)

这些“补丁”给开发者带来心智负担,也是 Vue 3 用 Proxy 重写响应式系统的最直接原因——Proxy 天然支持属性增删检测和数组索引拦截,我们在下一节详述。