Code前端首页关于Code前端联系我们

Vue2里的Object.defineProperty到底在干啥?

terry 11小时前 阅读数 9 #Vue
文章标签 defineProperty

说起Vue2的响应式原理,绕不开JavaScript里的Object.defineProperty这个“老工具”,不少刚入门Vue的同学总会疑惑:它到底在Vue里承担啥功能?为啥Vue2偏偏选它实现数据响应?实际开发时又得注意哪些隐藏的“小陷阱”?今天咱们从原理到实践,把这些问题拆开来唠明白。

Object.defineProperty是啥?和Vue2响应式有啥关系?

JavaScript里,Object.defineProperty是给对象“定制属性”的工具,比如你想控制一个属性能不能被修改、能不能被遍历,甚至读取和赋值时做额外操作,都能靠它实现,它的语法长这样:Object.defineProperty(目标对象, 属性名, 配置对象),配置对象里可以写value(属性值)、writable(是否可写)、enumerable(是否可枚举),还有关键的getset——这俩就是Vue2响应式的“抓手”。

Vue2的核心是“数据变了,视图自动更新”,那怎么知道数据变了?就得靠“劫持”数据的读取和修改操作,比如有个数据对象data={name:'小明'},Vue会用Object.defineProperty重新定义name这个属性,给它加上getter(读取时触发)和setter(修改时触发),读取name时,getter会把当前组件的“观察者”(Watcher)记下来;修改name时,setter会通知这些观察者:“数据变啦,快去更新视图!”——这就是响应式的基本逻辑。

Vue2为啥选Object.defineProperty,不用其他方法?

得回到Vue2诞生的时间(2016年前后)看技术环境,那时候ES6的Proxy还没被浏览器广泛支持,很多老浏览器(比如IE)根本不认识Proxy,而Object.defineProperty是ES5的特性,兼容性更好,能覆盖更多用户。

要是不用Object.defineProperty,直接用普通赋值(比如obj.name='新值'),根本没法“监听”这个赋值动作,但Object.defineProperty能通过getset拦截读写,这就给了Vue“暗中观察”数据变化的能力,所以在当时的技术条件下,它是实现响应式最靠谱的选择。

用Object.defineProperty实现响应式,具体咋运作?

咱们自己模拟Vue做个简单响应式,理解核心逻辑:

let data = { name: '小红' }
let target = null // 用来存当前的“观察者”
// 模拟Watcher,负责更新视图
function Watcher(cb) {
  target = cb
  target() // 执行cb时会触发get,把自己加入依赖
  target = null
}
// 定义响应式对象(简化版,实际Vue有Dep管理依赖)
function defineReactive(obj, key, value) {
  Object.defineProperty(obj, key, {
    get() {
      // 读取时,把当前Watcher存起来(依赖收集)
      if (target) {
        dep.add(target) // 实际Vue用Dep类管理依赖,这里简化示意
      }
      return value
    },
    set(newVal) {
      if (newVal === value) return
      value = newVal
      // 数据变化,通知所有Watcher更新(派发更新)
      dep.notify() 
    }
  })
}
// 初始化data的响应式
function observe(obj) {
  for (let key in obj) {
    defineReactive(obj, key, obj[key])
  }
}
observe(data)
// 创建Watcher,当数据变化时更新视图
new Watcher(() => {
  document.querySelector('#app').innerText = data.name
})
// 模拟修改数据
data.name = '小绿' // 触发set,视图自动更新

这段代码里,Object.definePropertygetter负责“记下来谁用了这个数据”(依赖收集),setter负责“告诉这些使用者:数据变了,快更新”(派发更新),Vue的响应式核心逻辑和这个思路一致,只是把Watcher、Dep(依赖管理)这些细节做得更完善。

Object.defineProperty在Vue2里有哪些“小缺点”?

数组的特殊处理

数组的pushpop这些方法,Object.defineProperty管不住,因为数组是按索引存元素的,但我们常用的push是修改数组长度,没法给每个数组方法加get/set,所以Vue2专门“重写”了数组的原型方法,比如把Array.prototype.push改成自己的版本,这样调用push时能触发更新,但如果直接用下标改元素(比如arr[0] = '新值'),Vue2就监听不到,得用this.$set(arr, 0, '新值')

举个开发场景:做待办列表时,数组todos存任务,用todos.push(newTodo)能触发更新(因为push被重写了);但直接改todos[0].name = '新名称',视图不会变——这时候得用this.$set(todos[0], 'name', '新名称'),或者替换整个数组(todos = todos.map(...))。

对象新增属性不响应

假如有个对象user={name:'张三'},后来给user加个age属性:user.age=18,这时候age没被Object.defineProperty劫持过,所以修改age不会触发视图更新,得用this.$set(user, 'age', 18),让Vue给age也加上get/set

比如组件里datauser: { name: '李四' },模板显示{{ user.age }},如果在方法里直接写this.user.age = 20,页面上age还是空的;必须用this.$set(this.user, 'age', 20),新属性才会被劫持,后续修改也能触发更新。

深层对象的性能开销

如果数据是多层嵌套的(比如user.info.address.city),Vue2要递归遍历每个属性,给每层都加Object.defineProperty,如果对象特别深、属性特别多,初始化时的递归会影响性能,而且如果后续才用到深层属性,提前递归劫持就有点“浪费”。

和Vue3的Proxy比,Object.defineProperty差在哪?

Vue3换成Proxy,核心是Proxy能“代理整个对象”,而不是像Object.defineProperty那样逐个属性处理。

  • 对象新增/删除属性Proxy能通过set/deleteProperty拦截,不用像Vue2那样靠$set手动处理。
  • 数组操作:不管是pushpop还是下标修改(arr[0] = '新值'),Proxy都能监听到,不用Vue2那样单独重写数组方法。
  • 性能与懒代理Proxy是懒代理,比如深层对象可以等用到某一层时再去代理,不用一开始就递归遍历所有属性,性能更优。

但Vue2那时候没法用Proxy!2016年前后,IE浏览器还没被淘汰,Proxy在IE里完全不支持,而Object.defineProperty是ES5的,兼容性好,所以Vue2选Object.defineProperty是当时的无奈之举,Vue3等到浏览器生态更友好了,才升级成Proxy,这也体现了前端技术迭代里“兼容性”和“先进性”的平衡。

实际开发中,怎么避开Object.defineProperty带来的“坑”?

数组操作:优先用变异方法或替换数组

如果要让数组变化触发更新,优先用Vue提供的变异方法pushpopsplice等,这些Vue重写过的),要是想替换数组,用新数组覆盖(比如arr = [...arr, 新元素]),因为数组引用变了,Vue能检测到,如果非得用下标改元素,记得用this.$set(arr, 索引, 新值)

对象新增属性:用$set或对象替换

别直接给对象加新属性,要用this.$set或者Vue.set,比如给userage,写成this.$set(this.user, 'age', 18),这样新属性才会被劫持,如果是批量加属性,可以先把对象深拷贝一份,修改后再替换原来的对象(比如this.user = { ...this.user, age: 18, gender: '男' })。

深层对象处理:按需劫持+优化依赖

如果数据嵌套深,又怕递归劫持影响性能,可以考虑“按需劫持”,比如用户信息里的地址,只有编辑地址时才去处理深层属性的响应式,或者用计算属性watch来针对性监听,减少不必要的依赖收集。

比如用计算属性处理深层数据:

computed: {
  city() {
    return this.user.info.address.city
  }
}

这样只有city变化时才更新,不用深层监听整个user对象。

合理用计算属性和watch

计算属性会自动处理依赖,只有依赖变化才更新,比直接在模板里写复杂逻辑更高效。watch可以指定深度监听(deep: true),但深层监听要谨慎——因为每次对象深层属性变化都会触发,可能影响性能,尽量结合immediate、条件判断等优化。

从Object.defineProperty看前端框架的技术选型逻辑?

前端框架选技术方案,得看“当下能用”和“未来潜力”,Vue2选Object.defineProperty,是因为当时Proxy兼容性不够,而Object.defineProperty能覆盖更多用户,实现响应式的核心需求,等到Vue3的时候,浏览器对Proxy支持好了,生态也更成熟,就升级技术方案,解决Object.defineProperty的缺陷。

这和其他框架的演进逻辑一样,比如React从class组件到hooks,也是跟着JS语法发展、开发者体验优化走的,所以我们学框架时,不光要记住“怎么用”,还要想“为啥这么选”——技术选型背后是兼容性、性能、开发体验等多方面的权衡。

绕了一圈,你会发现Object.defineProperty在Vue2里是“时代的选择”——它撑起了Vue2响应式的半边天,也因为时代局限留下了些小遗憾,但正是这些技术选择和迭代,让我们看到前端框架是怎么在现实约束下做最优解,又怎么跟着技术发展持续进化的,下次再遇到Vue2的响应式问题,不妨想想Object.defineProperty的角色,或许能更通透地理解框架逻辑~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门