Vue 最核心的特性是“数据变化,视图自动更新”,这在 Vue 2 中是借助 Object.defineProperty 实现的。理解这一机制,不仅能帮你避坑,也能让你体会到 Vue 3 用 Proxy 重写响应式系统的原因。
核心思想:数据劫持 + 依赖收集 + 派发更新
Object.defineProperty 允许我们为一个对象的属性定义 get 和 set 拦截器。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”就被收集进去。当数据修改触发 set,dep.notify() 会通知所有 Watcher 重新渲染。
依赖收集全流程
- 每个组件实例都有一个对应的 Watcher(渲染 Watcher)。
- 组件首次渲染时,会触发 data 中属性的
get,此时会把组件的 Watcher 添加到该属性的依赖列表(Dep)中。 - 当属性值被修改时,
set中调用dep.notify(),遍历这个列表,让所有订阅的 Watcher 重新执行更新视图。
这就构建了一套“谁用了我,我就告诉谁我变了”的机制。关键点:Vue 2 是在 get 中依赖收集,在 set 中触发更新,因此只有被 defineProperty 处理过的属性才具备响应式能力。
数组的响应式特殊处理
由于 Object.defineProperty 可以直接劫持对象属性,但却无法拦截数组下标的直接修改(arr[0] = 'x'),也无法拦截 .length 的变化。因此,Vue 2 对数组做了两件事:
- 重写七种变更方法:
push、pop、shift、unshift、splice、sort、reverse。这些方法在执行原操作后,会手动触发依赖更新。 - 不处理索引与 length:直接通过索引设置元素(
arr[0] = newVal)或修改length,不会触发更新。这在开发中极易踩坑。
同时,对数组中每个元素(如果是对象)仍然会进行响应式处理,所以 arr.push({ name: 'Bob' }) 后,新对象的属性依然可响应。
原生缺陷:新增/删除属性不触发更新、数组索引修改不生效
Object.defineProperty 只能劫持对象已有的属性。这会带来三个典型问题:
- 新增属性:给 data 中的对象动态添加一个全新的属性,
obj.newProp = 'hello',Vue 2 无法检测。必须使用Vue.set(obj, 'key', value)或者this.$set()。 - 删除属性:
delete obj.prop同样不会触发更新,需要使用Vue.delete(obj, 'key')。 - 数组索引直接赋值:
arr[0] = newItem无效,需要用Vue.set(arr, 0, newItem)或arr.splice(0, 1, newItem)。
这些“补丁”给开发者带来心智负担,也是 Vue 3 用 Proxy 重写响应式系统的最直接原因——Proxy 天然支持属性增删检测和数组索引拦截,我们在下一节详述。