详细介绍一下defineproperty 和 Proxy

文章对比了Vue.js2中的Object.defineProperty和Vue.js3中的Proxy在实现数据响应式上的差异。Object.defineProperty只能针对已有属性进行拦截,无法处理属性的添加和删除,以及数组操作,需要额外的set和deleteAPI。而Proxy则能全局拦截对象的所有操作,包括属性添加和删除,且对数组操作支持更好,提供更广泛的数据响应能力。Vue3.0采用Proxy主要是因为其更好的性能和灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

`Object.defineProperty`和`Proxy`是Vue.js 2和Vue.js 3中用于实现数据响应式的两种不同的机制。当需要对对象进行拦截和监视时,JavaScript提供了两种主要的机制:`Object.defineProperty`和`Proxy`。

一、Object.defineProperty

Vue.js 2中使用的是`Object.defineProperty`来实现数据响应式。它通过在对象上定义访问器属性来拦截对属性的访问和修改操作,并在这些操作发生时触发相关的更新操作。具体来说,它会在对象上定义一个getter和setter函数,使得当属性被访问或修改时,能够触发依赖追踪和更新。这样,当数据发生变化时,Vue能够检测到变化并及时更新相关的视图。

(1)`Object.defineProperty`是一个用于修改现有对象属性特性或定义新属性的方法。
(2)通过`Object.defineProperty`可以定义属性的可写性(writable)、可枚举性(enumerable)和可配置性(configurable)。
(3)可以使用`get`和`set`方法定义属性的读取和写入行为,从而实现对属性的拦截和监视。

get:属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值

set:属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined

(4)例子:   

定义一个响应式函数defineReactive

function update() {
  app.innerText = obj.foo
}

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal
        update()
      }
    },
  })
}

调用defineReactive,数据发生变化触发update方法,实现数据响应式

const obj = {}
defineReactive(obj, 'foo', '')
setTimeout(() => {
  obj.foo = new Date().toLocaleTimeString()
}, 1000)

在对象存在多个key情况下,需要进行遍历

function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  Object.keys(obj).forEach((key) => {
    defineReactive(obj, key, obj[key])
  })
}

如果存在嵌套对象的情况,还需要在defineReactive中进行递归

function defineReactive(obj, key, val) {
  observe(val)
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal
        update()
      }
    },
  })
}

当给key赋值为对象的时候,还需要在set属性中进行递归

set(newVal) {
    if (newVal !== val) {
        observe(newVal) // 新值是对象的情况
        notifyUpdate()
    }
}

上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题

现在对一个对象进行删除与添加属性操作,无法劫持到

const obj = {
  foo: 'foo',
  bar: 'bar',
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok

当我们对一个数组进行监听的时候,并不那么好使了

const arrData = [1, 2, 3, 4, 5]
arrData.forEach((val, index) => {
  defineProperty(arrData, index, val)
})
arrData.push() // no ok
arrData.pop() // no ok
arrDate[0] = 99 // ok

`Object.defineProperty`适用于对现有对象进行属性级别的拦截和监视,但它只能对已有的属性进行定义,无法对整个对象进行拦截。

可以看到数据的api无法劫持到,从而无法实现数据响应式,

所以在Vue2中,增加了setdelete API,并且对数组api方法进行一个重写

还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题

总结:

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

二、Proxy

Vue.js 3中引入了`Proxy`作为实现数据响应式的新机制。`Proxy`是ES6中提供的一个强大的对象拦截器,它可以对目标对象进行拦截,并在拦截操作发生时执行自定义的操作。在Vue.js 3中,使用`Proxy`来代理对象,并在访问或修改属性时拦截操作并触发更新。相比`Object.defineProperty`,`Proxy`提供了更广泛和灵活的拦截能力。

(1)`Proxy`是ES6引入的一种强大而灵活的对象拦截和监视机制。
(2)通过创建`Proxy`对象,可以在目标对象上拦截并定制各种操作,如属性的读取(get)、写入(set)、删除(delete)、函数调用(apply)等。
(3)`Proxy`提供了一系列的`handler`方法,用于定义拦截操作的处理逻辑。
(4)例子:

定义一个响应式方法reactive

function reactive(obj) {
  if (typeof obj !== 'object' && obj != null) {
    return obj
  }
  // Proxy相当于在对象外层加拦截
  const observed = new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      console.log(`获取${key}:${res}`)
      return res
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver)
      console.log(`设置${key}:${value}`)
      return res
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key)
      console.log(`删除${key}:${res}`)
      return res
    },
  })
  return observed
}

测试一下简单数据的操作,发现都能劫持

const state = reactive({
  foo: 'foo',
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok

再测试嵌套对象情况,这时候发现就不那么 OK 了

const state = reactive({
  bar: { a: 1 },
})

// 设置嵌套对象属性
state.bar.a = 10 // no ok

如果要解决,需要在get之上再进行一层代理

function reactive(obj) {
    if (typeof obj !== 'object' && obj != null) {
        return obj
    }
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            return isObject(res) ? reactive(res) : res
        },
    return observed
}

总结:

Object.defineProperty只能遍历对象属性进行劫持

function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  Object.keys(obj).forEach((key) => {
    defineReactive(obj, key, obj[key])
  })
}

Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的

function reactive(obj) {
  if (typeof obj !== 'object' && obj != null) {
    return obj
  }
  // Proxy相当于在对象外层加拦截
  const observed = new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      console.log(`获取${key}:${res}`)
      return res
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver)
      console.log(`设置${key}:${value}`)
      return res
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key)
      console.log(`删除${key}:${res}`)
      return res
    },
  })
  return observed
}

Proxy可以直接监听数组的变化(pushshiftsplice

const obj = [1, 2, 3]
const proxtObj = reactive(obj)
obj.psuh(4) // ok

Proxy有多达 13 种拦截方法,不限于applyownKeysdeletePropertyhas等等,这是Object.defineProperty不具备的

正因为defineProperty自身的缺陷,导致Vue2在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外setdelete方法)

// 数组重写
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
  arrayProto[method] = function () {
    originalProto[method].apply(this.arguments)
    dep.notice()
  }
});

// set、delete
Vue.set(obj,'bar','newbar')
Vue.delete(obj),'bar')

`Proxy`提供了更广泛的拦截和监视能力,可以对目标对象的各种操作进行拦截和定制处理逻辑。
可以根据需要使用不同的`handler`方法,如`get`、`set`、`deleteProperty`、`apply`等,来实现特定操作的拦截和监视。

总结:
`Object.defineProperty`是一种用于修改对象属性特性或定义新属性的方法,适用于属性级别的拦截和监视。而`Proxy`是一种更强大和灵活的对象拦截和监视机制,可以对目标对象的各种操作进行拦截和定制处理逻辑。根据需求和场景,可以选择合适的机制来实现对象的拦截和监视。

三、Vue3.0 里为什么要用 Proxy API 替代 defineProperty API 

最核心的原因是性能,展开来说如下。

  • 因为 Proxy 代理的直接是整个对象,例如对象中有 100 个属性,用 defineProperty 劫持至少需要循环 100 次,而 proxy 至少一次搞定。

  • defineProperty 对数组中存在大量元素的劫持操作性能不好,所以 Vue2 并没有直接使用 defineProperty 对数组进行劫持,而是提供了额外的方法来处理数组的响应式,例如 $set,其实换成 proxy 就不存在这个问题了,当然 $set 方法也就没有必要存在了。

  • Vue2 中,劫持数据的操作在实例创建完成就已经进行完毕了,所以对对象后续新增的属性是劫持不到的,也就意味着后续新增的属性是不具备响应式能力的,所以 Vue 不得不提供了 $set 方法。而换成 proxy 就不存在这个问题了,因为它劫持的整个对象,后续添加的属性也是属于这个对象的,那么 $set 也就没有必要了(干掉 $set 方法本身也是性能的一个体现)。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值