← Technology Archive

Historical archive

Vue 2 vs. Vue 3 Reactivity: defineProperty, Proxy, and Reflect

How Object.defineProperty powers basic interception, why Vue 2 had reactivity limitations, and how Proxy and Reflect support Vue 3's object-level reactivity.

Object.defineProperty()

Object.defineProperty(obj, prop, descriptor) defines one property on an object:

  • obj: the object to modify
  • prop: the property name
  • descriptor: configuration for the property

Object.defineProperties() defines several properties at once.

Data descriptors

A property descriptor can contain:

  • value
  • writable, which defaults to false
  • enumerable, which defaults to false
  • configurable, which defaults to false
function createObject() {
  var object = {}

  Object.defineProperties(object, {
    a: {
      value: 1,
      writable: true,
      enumerable: true,
      configurable: true,
    },
    b: {
      value: 2,
    },
  })

  return object
}

var object = createObject()
object.a = 5
object.b = 5
console.log(object) // { a: 5, b: 2 }

for (var key in object) {
  console.log(key + ':' + object[key])
  // Only a is enumerable.
}

delete object.a
delete object.b
console.log(object) // { b: 2 }

Intercept access with getters and setters

A descriptor cannot combine value or writable with get or set.

function createObject() {
  var object = {}
  var a = 1

  Object.defineProperties(object, {
    a: {
      get() {
        return `"a"'s value is ${a}.`
      },
      set(newValue) {
        a = newValue
        console.log(`a was assigned ${newValue}`)
      },
    },
    b: {},
  })

  return object
}

var object = createObject()
console.log(object.a)
object.a = 2

Record assigned values

function DataArray() {
  var value = null
  var history = []

  Object.defineProperty(this, 'value', {
    get: function () {
      return value
    },
    set: function (newValue) {
      value = newValue
      history.push({ value })
      console.log(`A new value "${value}" was added`)
    },
  })

  this.getHistory = function () {
    return history
  }
}

var data = new DataArray()
data.value = 123
data.value = 234
console.log(data.getHistory())

A minimal Vue 2-style binding

This simplified example updates a paragraph whenever the intercepted property changes:

<p>0</p>

<script>
  function createObject() {
    var object = {}
    var a = 1

    Object.defineProperty(object, 'a', {
      get() {
        return a
      },
      set(newValue) {
        a = newValue
        document.getElementsByTagName('p')[0].innerHTML = a
      },
    })

    return object
  }

  var object = createObject()
  object.a = 2
</script>

Vue 2 built reactivity around property-level getter and setter interception. Because interception was attached per property, adding properties and observing certain array operations required special handling.

Internal object operations

JavaScript objects support a set of internal operations reflected by familiar APIs:

var object = { a: 1, b: 2 }

// Prototype operations
Object.getPrototypeOf(object)
Object.setPrototypeOf(object, { c: 3 })

// Extensibility and integrity
Object.isExtensible(object)
Object.preventExtensions(object)
Object.seal(object)
Object.freeze(object)

// Own properties
Object.getOwnPropertyNames(object)
Object.defineProperty(object, 'c', { value: 3 })
object.hasOwnProperty('a')
Object.keys(object)

// Get, set, delete, and enumerate
console.log(object.a)
object.a = 3
delete object.a
for (var key in object) {
  console.log(object[key])
}

// Function calls and construction are also internal operations.
function Test() {}
Test.call(null)
new Test()

These operations matter because a Proxy can intercept many of them.

Proxy

Object.defineProperty() configures individual properties. An ES2015 Proxy wraps a target and can intercept operations across the target object.

const proxy = new Proxy(target, handler)
  • target is the object being wrapped.
  • handler contains traps for operations such as reading, assigning, enumerating, calling, or constructing.

A Proxy can wrap objects, arrays, and functions. This broader interception model is the foundation of Vue 3’s reactivity system.

Reflect

Reflect is a global object containing function-based versions of many internal object operations:

const value = Reflect.get(target, property)
const succeeded = Reflect.set(target, property, nextValue)

if (succeeded) {
  console.log('Set successfully')
}

Proxy traps commonly delegate the default operation to Reflect, then add tracking or notification behavior around it.