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()– interceptsinoperatorhandler.get()– intercepts property readshandler.set()– intercepts property writeshandler.deleteProperty()– interceptsdeletehandler.apply()– intercepts function callshandler.construct()– interceptsnew
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.