Vue Reactivity Principles

March 10, 2023

Before talking about reactivity, let's first discuss the ES6 additions: Proxy and Reflect.

Listening to Objects

Before ES6, we could actually use Object.defineProperty() to implement object listening:

Object.keys(obj).forEach(key => {
  let value = obj[key]
  Object.defineProperty(obj,key,{
    set(newValue){
      console.log(`Detected change in ${key}`)
      value = newValue
    },
    get(){
      return value
    }
  })
})

However, this approach has drawbacks: Object.defineProperty() was not designed for listening to objects. Moreover, if we want to listen to more complex operations like addition or deletion, this method cannot achieve that.

Proxy

In ES6, a new Proxy class was added. As its name suggests, it helps us create a proxy. That is, if we want to listen to an object's operations, we can first create a proxy object (Proxy). All operations on this proxy object can be intercepted, allowing us to monitor operations on the original object.

Implementing Listening with Proxy

We can rewrite the previous example using Proxy:

const obj = {
  name: 'curry',
  age: 33,
}

// Create a proxy object
// target is the target object, handler contains traps
const objProxy = new Proxy(obj, {})

Now we can operate directly on the proxy object:

const objProxy = new Proxy(obj, {
  get(target, key, receiver) {
    return target[key]
  },
  set(target, key, newValue, receiver) {
    target[key] = newValue
  },
  has(target, key, receiver) {
    return key in target
  },
  deleteProperty(target, key) {
    delete target[key]
  },
})

Proxy Traps

Proxy has 13 traps. Common ones include:

  • handler.has() – intercepts in operator
  • handler.get() – intercepts property reads
  • handler.set() – intercepts property writes
  • handler.deleteProperty() – intercepts delete
  • handler.apply() – intercepts function calls
  • handler.construct() – intercepts new

Reflect

Reflect is another ES6 addition. It's an object that provides reflection capabilities.

Purpose of Reflect

Reflect provides many methods to operate on objects, similar to methods on Object. Why introduce Reflect? Early ECMA specifications did not provide a standard design for object operations on constructors, so Reflect was introduced to make these operations more consistent.

Reflect Methods

Many methods in Reflect correspond to the 13 traps of Proxy.

Using Reflect

We can replace Object operations in Proxy traps with Reflect methods:

const objProxy = new Proxy(obj, {
  get(target, key) {
    return Reflect.get(target,key)
  },
  set(target, key, newValue) {
    Reflect.set(target,key,newValue)
  },
  has(target, key) {
    return Reflect.has(target,key)
  },
  deleteProperty(target, key) {
    Reflect.deleteProperty(target,key)
  },
})

Role of receiver in Proxy and Reflect

If the source object has getter/setter accessors, receiver allows us to change the this context inside them to point to the proxy object:

const obj = {
  _name: 'curry',
  age: 33,
  get name(){
    console.log(this)
    return this._name
  },
  set name(newValue){
    console.log(this)
    this._name = newValue
  }
}

const objProxy = new Proxy(obj, {
  get(target, key, receiver) {
    console.log('getter')
    return Reflect.get(target, key, receiver)
  },
  set(target, key, newValue, receiver) {
    console.log('setter')
    Reflect.set(target, key, newValue, receiver)
  },
})

Reflect.construct

Reflect.construct(target, argumentsList, newTarget) allows using one function as a constructor to create another type:

function Person(name,age){
  this.name = name
  this.age = age
}

function Coder(){}

const obj = Reflect.construct(Person,['curry',33], Coder)
console.log(obj.__proto__ == Coder.prototype) // true

Reactivity

After understanding Proxy and Reflect, let's talk about reactivity.

What is Reactivity?

let m = 10

// Code that needs to rerun when m changes
console.log(m)
console.log(m + 2)

m = 40

Reactivity is the mechanism that allows code to automatically respond to changes in data variables.

Reactive Function Design

We need to distinguish which functions need reactivity:

// Needs reactivity
function foo(){
  const value = obj.name
  console.log(value)
}

// Does not need reactivity
function bar(){
  let res = 1 + 1
  console.log(res)
}

Implementing Reactive Functions

const reactiveFns = []

function watchFn(fn) {
  reactiveFns.push(fn)
  fn()
}

watchFn(function(){
  const value = obj.name
  console.log(value)
})

Collecting Dependencies

Using a single reactiveFns array is not practical. Each object property needs its own set of reactive functions. We create a Depend class:

class Depend{
  constructor(){
    this.reactiveFns = []
  }

  addDepend(fn){
    this.reactiveFns.push(fn)
  }

  notify(){
    this.reactiveFns.forEach(fn => fn())
  }
}

Listening to Object Changes with Proxy & Reflect

const objProxy = new Proxy(obj, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
  },
})

Dependency Management with WeakMap

function getDepend(target, key) {
  let map = targetWeakMap.get(target)
  if (!map) {
    map = new Map()
    targetWeakMap.set(target, map)
  }

  let depend = map.get(key)
  if(!depend){
    depend = new Depend()
    map.set(key,depend)
  }

  return depend
}

Reactivity Implementation

function reactive(obj){
  return new Proxy(obj,{
    get(target, key, receiver) {
      const depend = getDepend(target,key)
      depend.addDepend()
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      const depend = getDepend(target,key)
      depend.notify()
    },
  })
}

Vue2 Reactivity

Vue2 uses Object.defineProperty() for reactivity:

function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get() {
        const depend = getDepend(obj,key)
        depend.addDepend()
        return value
      },
      set(newValue) {
        value = newValue
        const depend = getDepend(obj,key)
        depend.notify()
      },
    })
  })
  return obj
}

Summary

In this chapter, we introduced Proxy, Reflect, and the principles of reactivity. Step by step, we combined new ES6 features to implement object reactivity, and compared Vue2 and Vue3 reactivity implementations.