技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

为什么 Vue 3 的 reactive 不再主要使用 defineProperty?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/60-为什么Vue3 0不再使用defineProperty实现数据监听?
本文目录11 个章节
  1. 一、Object.defineProperty 真的不能监听数组下标吗
  2. 二、Vue 2.7 如何处理数组
  3. 三、Vue 2 的 Vue.set 做了什么
  4. 四、Proxy 和 Object.defineProperty 的差异
  5. 五、Vue 3 依赖收集的现代模型
  6. 六、Vue 3 并没有完全删除 defineProperty
  7. 七、为什么 Vue 3 选择 Proxy
  8. 八、浏览器兼容性与 Proxy polyfill
  9. 九、SSR 注意事项
  10. 十、总结
  11. 参考资料

为什么 Vue 3 的 reactive 不再主要使用 defineProperty

原文标题是“为什么 Vue 3.0 不再使用 defineProperty 实现数据监听”。更准确的说法是:Vue 3 的 reactive() 对象/数组响应式主要使用 Proxy,但 Vue 3 并没有完全不使用 getter/setter。 ref.value 仍然通过 getter/setter 追踪依赖,组件 public instance 也使用 Proxy 和其他访问器。

版本说明:本文以 Vue 2.7.16 与 Vue 3.5.39 的公开行为和固定源码为背景。Vue 2 已结束官方维护,新项目优先 Vue 3。

一、Object.defineProperty 真的不能监听数组下标吗

这句话需要分成两层理解:

  1. Object.defineProperty 本身可以给已经存在的数组下标属性定义 getter/setter;
  2. Vue 2 的 Observer 选择了另一种数组实现,没有为每个下标安装 getter/setter,因此 Vue 2 直接通过下标赋值不会自动更新。

先写一个只观察已有下标的实验:

function defineReactive(target, key, value) {
  Object.defineProperty(target, key, {
    configurable: true,
    enumerable: true,
    get() {
      console.log('get:', key)
      return value
    },
    set(nextValue) {
      console.log('set:', key, nextValue)
      value = nextValue
    },
  })
}

function observeExistingIndexes(array) {
  Object.keys(array).forEach(key => {
    defineReactive(array, key, array[key])
  })
}

const list = [1, 2, 3]
observeExistingIndexes(list)

console.log(list[0]) // 触发已有下标 0 的 getter
list[0] = 10       // 触发已有下标 0 的 setter
list.push(4)       // 新下标 3 没有被上面的函数定义 getter/setter

这个实验只能证明“已有属性可以被劫持”,不能直接代表 Vue 2 的数组 Observer。

二、Vue 2.7 如何处理数组

Vue 2.7 的 Observer 对数组做了单独处理:

  • 不为每个数组下标逐一定义 getter/setter;
  • 通过 arrayMethods 拦截七个变异方法;
  • pushunshiftsplice 插入的新元素递归 observe;
  • 调用数组自身的 Dep 通知依赖。

七个方法是:

push、pop、shift、unshift、splice、sort、reverse

可以把核心逻辑简化为:

const arrayMethods = Object.create(Array.prototype)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]

methodsToPatch.forEach(method => {
  const original = Array.prototype[method]

  Object.defineProperty(arrayMethods, method, {
    configurable: true,
    value(...args) {
      const result = original.apply(this, args)
      // Vue 2 实际还会 observe 插入项,并调用 ob.dep.notify()
      return result
    },
  })
})

因此 Vue 2 中以下写法不会自动触发响应式更新:

this.items[index] = nextValue
this.items.length = 0

应改为:

this.$set(this.items, index, nextValue)
this.items.splice(index, 1, nextValue)

或者在 Vue 2 中替换整个数组引用。Vue 2 官方文档说的是七个变异方法,不是“八种方法”。

参考:Vue 2 响应式变化检测限制Vue 2.7 Observer 源码

三、Vue 2 的 Vue.set 做了什么

Vue 2 对象新增属性也不能直接被自动观察:

this.user.age = 18 // 如果 age 原来不存在,Vue 2 不会自动追踪

可以使用:

this.$set(this.user, 'age', 18)
// 或 Vue.set(this.user, 'age', 18)

对数组下标,Vue.set 最终会转成 splice;对响应式对象的新 key,则会调用类似 defineReactive 的逻辑并通知依赖。根 Vue 实例和根 $data 还有不能动态扩展的限制。

四、Proxy 和 Object.defineProperty 的差异

4.1 监听对象本身的操作

Object.defineProperty 需要对每个已知属性单独定义 descriptor:

const state = {}
let value = 1

Object.defineProperty(state, 'count', {
  get: () => value,
  set: next => { value = next },
})

如果之后新增 state.name,原来的 descriptor 并不会自动覆盖它。Proxy 代理的是对象的整体操作:

const state = new Proxy(
  { count: 1 },
  {
    get(target, key, receiver) {
      console.log('读取', key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      console.log('设置', key, value)
      return Reflect.set(target, key, value, receiver)
    },
    deleteProperty(target, key) {
      console.log('删除', key)
      return Reflect.deleteProperty(target, key)
    },
  },
)

state.name = 'Vue 3' // 可以拦截新增属性
state.count++       // 可以拦截已有属性的读取和设置

Proxy 还可以拦截 hasownKeysgetOwnPropertyDescriptordefinePropertydeletePropertygetPrototypeOfsetPrototypeOfisExtensiblepreventExtensions 等操作;applyconstruct 只适用于函数代理,不是所有普通对象都能使用的 trap。

4.2 惰性代理与新增/删除

Vue 3 的 reactive() 通常在读取嵌套对象时惰性创建 Proxy,并通过 WeakMap 缓存代理:

import { reactive } from 'vue'

const state = reactive({
  user: { name: 'Vue' },
  items: [1, 2, 3],
})

state.user.name = 'Vue 3'
state.user.age = 18
state.items[1] = 20
state.items.length = 1
delete state.user.age

这些操作都可以被 Vue 3 的响应式系统追踪,但 Proxy 并不意味着运行时完全不需要数组处理。Vue 3 仍有 arrayInstrumentations,用于数组迭代、includes/indexOf 身份查询和长度变化时的 tracking/batch 控制。

4.3 Map 和 Set

Vue 2 Observer 主要面向普通对象和数组,Vue 3 还对 Map、Set、WeakMap、WeakSet 提供 collection handlers:

import { reactive, watchEffect } from 'vue'

const state = reactive(new Map<string, number>())

watchEffect(() => {
  console.log(state.get('count'))
})

state.set('count', 1)
state.delete('count')

集合的 gethassize、迭代、addsetdeleteclear 有不同的依赖类型,不能简单地用“对象 get/set”解释全部行为。

五、Vue 3 依赖收集的现代模型

教学文章常把依赖系统画成:

对象 -> key -> Set<effect>

Vue 3.5 的概念可以进一步展开为:

WeakMap<rawTarget, Map<key, Dep>>
                         ↓
                    ReactiveEffect

读取属性时执行 track,设置、删除或迭代时执行 trigger。Vue 3.5 的 Dep 和 Link 用于维护订阅关系,computed、watch 和组件渲染 effect 通过 scheduler/batch 组织更新。内部结构属于实现细节,文章中的简化 Set 模型只用于建立直觉。

六、Vue 3 并没有完全删除 defineProperty

6.1 ref.value 仍然是访问器语义

import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
  console.log(count.value)
})

count.value++

ref 对象的 .value 通过 getter 进行 track,通过 setter 进行 trigger。也就是说,“Vue 3 完全不使用 defineProperty”是错误的;准确说法是 reactive() 的对象/数组代理不再依靠逐属性 defineProperty。

6.2 组件实例的 public proxy

Vue 3 Options API 中的 this 访问也经过组件 public instance Proxy。setupState、data、props、ctx 和公共属性由不同的访问规则组成。Composition API 则不依赖 this,使用 ref/reactive 和闭包变量。

6.3 reactive 的边界

import { reactive, ref, toRefs } from 'vue'

const state = reactive({ count: 0 })
state.count++

// reactive 只能接收对象类型,不能直接包 primitive
const count = ref(0)

// 解构 reactive 时使用 toRefs 保留响应式连接
const { count: countFromState } = toRefs(state)

还需要注意:

  • raw 对象和 proxy 的身份不同,应该持续使用 reactive 返回的 proxy;
  • reactive() 绑定本身不能整体替换为另一个对象;
  • 直接解构 reactive 属性会断开响应式连接,应使用 toRef/toRefs
  • 深层对象默认会递归代理;
  • 普通对象中的 ref 可能自动解包,但数组整数元素和 Map/Set 条目不会按同样规则自动解包。

七、为什么 Vue 3 选择 Proxy

可以从几个方面概括:

7.1 能观察属性新增和删除

defineProperty 只能为已经定义的属性安装 descriptor;Proxy 可以拦截 setdeleteProperty,因此 Vue 3 不需要 Vue 2 的 Vue.set 来补充普通对象新属性。

7.2 能覆盖更多对象操作

Proxy 可以统一处理读取、设置、删除、in、遍历、原型和集合操作,更适合实现完整的响应式语义。

7.3 数组边界更自然

Vue 3 可以直接追踪数组 index 和 length,不需要 Vue 2 那套通过七个变异方法弥补下标响应式的设计。不过数组查询和批处理仍有专门 instrumentation,不能说“Proxy 让数组完全不需要运行时处理”。

7.4 不需要初始化时深度遍历所有属性

Vue 2 需要在 Observer 阶段递归转换对象;Vue 3 的嵌套对象通常在读取时惰性代理。这可能减少初始化工作,但 Proxy 和依赖追踪仍然有运行时成本,不能用“Proxy 必然更快”代替 profile。

八、浏览器兼容性与 Proxy polyfill

Proxy 是 Vue 3 的平台基础能力之一。Vue 3 不支持 IE11;如果必须支持 IE 等不具备原生 Proxy 的浏览器,应评估继续使用 Vue 2 或更换兼容方案。

某些 Proxy polyfill 只能覆盖少数 trap,可能要求创建代理时属性已知,对数组和完整反射语义支持不足,不能补齐 Vue 3 响应式所需的完整 Proxy 能力。不要把第三方 proxy-polyfill 的浏览器范围宣传成“Vue 3 全面兼容”。

参考:Vue FAQ:浏览器支持Google proxy-polyfill

九、SSR 注意事项

Vue 3 SSR 在服务端输出 HTML,服务端不会执行依赖浏览器 DOM 的 mounted/onMounted 逻辑。不要在服务端初始化阶段直接读取 windowdocument 或随机生成会影响 HTML 的值。

每个 SSR 请求应使用独立的 app、router、store 和请求级状态,避免不同用户之间共享响应式对象。客户端 hydration 还要求服务端和客户端首次输出一致。

十、总结

  1. Object.defineProperty 本身可以劫持已有数组下标,但 Vue 2 没有为数组下标逐一安装 getter;
  2. Vue 2 通过七个数组变异方法、Vue.setsplice 弥补响应式限制;
  3. Vue 3 reactive() 主要使用 Proxy,能追踪新增、删除、数组下标、length 和集合操作;
  4. Vue 3 仍在 ref.value 等位置使用 getter/setter,因此不能说完全不使用 defineProperty
  5. Vue 3 的 Proxy 也不是无成本或无需数组 instrumentation;
  6. IE 不支持原生 Proxy,polyfill 不能完整模拟 Vue 3 所需语义;
  7. 具体源码要固定到 Vue 2.7.16、Vue 3.5.39 等版本阅读,不要把旧截图或简化伪代码当成当前源码。

参考资料

原文主题和数组响应式实验予以保留;Proxy、ref、数组 instrumentation 和兼容性结论已按 Vue 2.7/Vue 3.5 官方资料重新整理。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS