Skip to content

Vue响应式原理中, Object.definePropty有什么缺陷?为什么在Vue3.0采用了Proxy,抛弃了Object.definePropty?

Object.definePropty

该方法允许精确地添加或修改对象的属性,并返回对象。默认情况下此方法添加的属性是不可修改的。

用法:

typescript
Object.definePropty(obj,prop,descriptor)

返回值:返回更新或者添加属性后的obj对象。

参数:

  • obj:要定义属性的对象。

  • prop:要添加或修改的属性名。

  • descriptor:属性描述符对象。

    包含以下属性的对象:

    value:属性的值,默认值是undefined

    writable:属性是否可写。默认值是false

    enumerable:属性的可枚举性,默认值是false,即属性不可枚举(或者说目标属性在 for…in、Object.keys、JSON.stringify 中是否可遍历)。

    configurable:属性的可配置性。默认为false,即属性不可被删除。

    get:获取属性值的函数。

    set:设置属性值的函数。

注意:

Object.defineProperty设置了gettersetter之后,就不能设置value属性。否则报错:

Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute,

Proxy

Proxy对象创建一个对象的代理,从而实现基本操作的拦截和自定义。

用法:

typescript
const _my_proxy = new Proxy(target,handler)

参数:

  • target:需要被代理的对象。通常是引用数据类型(数组,函数,对象等)。

  • handler:一个配置对象,具有以下配置选项。

    • handler.apply()

    • handler.construct()

    • handler.defineProperty()

    • handler.deleteProperty()

    • handler.get() 拦截对象属性的读写操作。

    • handler.getOwnPropertyDescriptor()

    • handler.getPrototypeOf()

    • handler.has() 拦截in操作符,判断对象是否包含某个属性。

    • handler.isExtensible()

    • handler.ownKeys()

    • handler.preventExtensions()

    • handler.set() 拦截对象属性的赋值操作。

    • handler.setPrototypeOf()

Object.definePropty有什么缺陷?

  1. 无法监听数组的变化。
  2. 无法监听对象属性的添加或删除
  3. 性能问题:这个api需要递归遍历对象的所有属性并为其添加gettersetter,者在对象负责或者嵌套层级较深时会影响性能。

Proxy的优势

  1. 全面监听对象和数组
  2. 性能提升:Proxy在对象层级较深时,只在访问属性时才会进行代理,性能更好。

Vue 3.0采用Proxy替代Object.defineProperty,主要因为Proxy能更全面、高效地监听对象和数组的变化,解决了Object.defineProperty在数组监听、属性增删和性能上的缺陷,同时提供了更好的扩展性和兼容性。

Vue的响应式原理

vue的响应式原理是在getter,setter的基础上结合发布-订阅模式实现。

Vue响应式系统:

typescript
import Observe from './Observe'
import Compile from './Compile'

export class Vue<D extends Record<string, any>> {
  public $el: string
  public $data: D

  constructor(options: { el: string; data: D }) {
    this.$el = options.el
    this.$data = options.data

    new Observe(this.$data)
    this.proxyData(this.$data)
    new Compile(this, this.$el)
  }

  //设置代理,拦截Vue实例的属性,可以直接在Vue实例上访问响应式数据
  private proxyData(data) {
    for (const [key] of Object.entries(data)) {
      Object.defineProperty(this, key, {
        get() {
          return data[key]
        },
        set(v) {
          data[key] = v
        },
      })
    }
  }
}

// 工厂函数:返回 Vue 实例 & data 属性的交叉类型
function createApp<D extends Record<string, any>>(options: { el: string; data: D }): Vue<D> & D {
  const instance = new Vue(options)
  return instance as Vue<D> & D
}

export { createApp }

定义一个观察者,观察者负责递归地对响应式数据进行拦截。当数据被设置时,通知订阅了此响应式数据的watcher进行数据更新。观察者代码如下:

typescript
import { protoType } from './ArrayProtoType'
import { watchers } from './Watcher'

//对Vue data字段的各个属性进行数据劫持
export default class Observe {
  constructor(public data) {
    this.walk(data)
  }

  public walk(data) {
    if (typeof data !== 'object' || data == null) return
    if (Array.isArray(data)) {
      //重置原型对象
      Object.setPrototypeOf(data, protoType)
    }

    Object.keys(data).forEach((key) => {
      this.definedReactive(data, key, data[key])
    })
  }

  public definedReactive(data: any, key: string, value: any) {
    this.walk(value)
    Object.defineProperty(data, key, {
      get() {
        return value
      },
      set: (v) => {
        if (v == value) return
        this.walk(v)
        value = v

        watchers.forEach((w) => w.update())
      },
    })
  }
}

定义订阅者,当订阅的数据发生变化时,操作DOM执行更新:

typescript
import type { Vue } from './Vue'

export const watchers = [] as Watcher[]

export default class Watcher {
  public vm: Vue<any>
  public name: string
  public fn: (v: any) => void

  constructor(vm: Vue<any>,attr: string, fn: (v: any) => void) {
    this.vm = vm
    this.name = attr
    this.fn = fn
    watchers.push(this)
  }

  public update() {
    this.fn(this.vm[this.name])
  }
}

完整代码:

Last updated: