What is reactive?
In simple terms, when we have data that changes, the code that depends on that data is re-executed. In Vue, for example, when our data changes, components that have references to that data on the interface are re-rendered.
How to implement responsiveness?
Purely manual implementation of responsiveness
- The easiest way to be responsive is to manually execute the code that needs to be re-executed when the data changes.
// We want b to always be twice as much as A
let a = 2
let b = a * 2
// When a changes, b is not twice as large as A.
a = 4
// If we want b to be twice as large as A, we need to re-execute b = a * 2
b = a * 2
Copy the code
- The above implementation seems a little silly, because every time a changes, we need to do it manually
b = a * 2
This line of code. - How to make
b = a * 2
Can this line of code be executed automatically? Want to makeb = a * 2
This line of code executes automatically, soYou need to know when a changes.That is to say,You need to be able to listen for changes in A.
How do I listen for changes in some properties of an object?
useObject.defineProperty
Listening to the object
You can use the Object.defineProperty API implementation to listen for Object property changes. This is how Vue2 is implemented in a responsive manner. If you’re not familiar with the Object.defineProperty API, you can check it out here.
Code implementation
let obj = {
name: 'aaa'.age: 18
}
// Get all keys of the obj object
const keys = Object.keys(obj)
// Iterate through the keys array, processing each property of obj
keys.forEach(key= > {
// Use the value variable to hold the attribute value corresponding to the key
let value = obj[key]
// Handle with object.defineProperty
Object.defineProperty(obj, key, {
get() { // When you get the property, it comes here
console.log(`${key}Property is acquired)
return value
},
set(newValue) { // When the property is modified, it will come here. And the set value is passed to the newValue parameter
console.log(`${key}Property modified ')
// obj[key] = newValue
// If you change the value of this property, it will enter set again, so that the loop is endless.
value = newValue
}
})
})
// Now we can listen to read and modify obj objects
console.log(obj.name) // 'name attribute fetched 'is printed before 'aaa', that is, listening for attribute fetched.
obj.name = 'bbb' // Prints that the name attribute has been modified, meaning that the attribute has been monitored for changes
Copy the code
Use ES6 Proxy to implement listening objects.
- use
Object.defineProperty
It is possible, though, to implement properties of listening objects. But itThere is no way toDo it forObject to listen, and there is no way to listen for data. That’s why new attributes in Vue2 need to be used if they need to be reactive, right$setTo implement, listening on arrays requires reworking some of the methods on the Array prototype. - ES6 launched
Proxy
This API, this API is used to implement listening objects, and this API is also effective for arrays. In the use ofProxy
“, usually matchReflect
Use together.If you are not familiar with Proxy, you can click here to view it.If You’re not familiar with Reflect you can check it out here
Code implementation
let obj = {
name: 'aaa'.age: 18
}
// The first argument is the object to be propped up, and the second argument is handler.
const proxy = new Proxy(obj, {
// When a property is accessed, the getter is called.
// Three arguments are passed simultaneously.
// target specifies the object to be proxied, in this case obj
// key Specifies the attribute to be accessed
// receiver is used to bind this
get(target, key, receiver) {
console.log(`${key}Property is accessed)
return Reflect.get(target, key, receiver)
},
// When you modify a property, you come to that Setter
// Four arguments are passed simultaneously
// target specifies the object to be proxied, in this case obj
// key Specifies the attribute to be accessed
// newValue specifies the newValue
// receiver is used to bind this
set(target, key, newValue, receiver) {
console.log(`${key}Property modified ')
return Reflect.set(target, key, newValue, receiver)
},
// There are plenty of other handlers for writing, but we won't write here.
})
// After executing the code above, the resulting proxy object is the proxy of the obj object.
// We only need to modify the proxy object to achieve the effect of the original object
// And the changes we make to the proxy object are ones we can listen to.
console.log(proxy.name) // 'name attribute fetched 'is printed before 'aaa', that is, listening for attribute fetched.
proxy.name = 'bbb' // Prints that the name attribute has been modified, meaning that the attribute has been monitored for changes
Copy the code
Now that we can listen on objects, what else do we need to do to implement responsiveness? We also need to know which code needs to be re-executed when a property is changed, i.e. which code depends on the property.
How is the collection of dependencies implemented?
Create a dependent class
class Dep {
constructor() {
this.effects = new Set()}// Add dependencies
addDep(){}// Re-enforce all dependencies
notify() {
this.effects.forEach(effect= > {
effect()
})
}
}
Copy the code
- The above Dep class is the one we use to collect dependencies. Storing the side effect function in a Set prevents the same side effect function from being executed more than once. When a property changes, notify executes all of the side effects functions to be responsive.
- The side effect function is mentioned above. A side effect function is simply a function that needs to be re-executed when a property changes. It is called a side effect function.
- To re-execute the side effect function, we have toCollecting side effect functions. We can implement something similar to Vue3
watchEffect
Function to collect side effect functions.
WatchEffect function
// Use a global variable to hold the current side effect function, which is also Vue's practice.
let effect = null
// This function takes a side effect function as an argument
watchEffect(fn) {
// Save fn to effect
effect = fn
// The watchEffect argument will be executed, so the fn function needs to be called
fn()
// Void effect
effect = null
}
Copy the code
- The watchEffect function saves fn to the global effect variable before executing the fn side effect function. Dependencies can be collected based on whether the effect has a value.
- With thisThe global variable of effect, we can implement the Dep class
addDep
Methods.
class Dep {
constructor() {
this.effects = new Set()}// Add dependencies
addDep() {
// Determine if effect has a value, and if so, collect the effect.
if (effect) {
this.effects.add(effect)
}
}
// Re-enforce all dependencies
notify() {
this.effects.forEach(effect= > {
effect()
})
}
}
Copy the code
- Implementation of dependency collection, as well as data monitoring. You also need to save dependencies and data correspondence. For example, when the A property of an OBj object changes, we need to know which functions depend on the A property of an obj object so that we can re-execute those dependent functions.
Implement dependencies and attribute correspondence.
To implement the dependency and attribute correspondence, you must select an appropriate data structure to store the correspondence. We can use WeakMap and Map to save. Click here if you are not familiar with Map. If you are not familiar with WeakMap, you can click here.
Why use WeakMap and Map
When the property of an object changes, we can get the object and the key of the property. Then we can use this object as the key of WeakMap, and the corresponding Value is a Map object. The Map’s key is the property of the listener, and the value is the dependency of the collected property.
So once you have this data structure, you need toWrite a function that fetches dependencies from the data structure.
Code implementation
// Save the corresponding relationship
const weakMap = new WeakMap(a)function getDep(target, key) {
// The targetDep obtained here is equivalent to the Map object shown above
let targetDep = weakMap.get(target)
// Check whether targetDep exists
if(! targetDep) {// If it does not exist, add it
weakMap.set(target, new Map())
// Get the Map object again
targetDep = weakMap.get(target)
}
// keyDep equals value in the Map above
let keyDep = targetDep.get(key)
// Check whether keyDep exists
if(! keyDep) {// If not, add a Dep instance
targetDep.set(key, new Dep())
keyDep = targetDep.get(key)
}
// Return dependency
return keyDep
}
Copy the code
Having implemented the above step by step, we now just need to combine everything above to implement the reactive form.
Responsive complete code implementation
// The dependent class
class Dep {
constructor() {
this.effects = new Set()}addDep() {
if (effect) {
this.effects.add(effect)
}
}
notify() {
this.effects.forEach(effect= > {
effect()
})
}
}
// The global variable is used to hold the currently executed side effect function
let effect = null
function watchEffect(fn) {
effect = fn
fn()
effect = null
}
// Use weakMap to save the corresponding relationship
const weakMap = new WeakMap(a)// Get the dependent function
function getDep(target, key) {
// The targetDep obtained here is equivalent to the Map object shown above
let targetDep = weakMap.get(target)
// Check whether targetDep exists
if(! targetDep) {// If it does not exist, add it
weakMap.set(target, new Map())
// Get the Map object again
targetDep = weakMap.get(target)
}
// keyDep equals value in the Map above
let keyDep = targetDep.get(key)
// Check whether keyDep exists
if(! keyDep) {// If not, add a Dep instance
targetDep.set(key, new Dep())
keyDep = targetDep.get(key)
}
// Return dependency
return keyDep
}
// Encapsulate the above listener into a function.
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// When the side effect function is executed, the properties of the Proxy object are accessed and listened on here.
// Get the deP instance
const dep = getDep(target, key)
// Call the addDep method of the DEP instance to collect dependencies
dep.addDep()
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
// When you modify the property value, it will come here. We need this to trigger notify of the DEP instance
// Change the property value first
Reflect.set(target, key, newValue, receiver)
// Get the deP instance
const dep = getDep(target, key)
// Call notify to re-execute all side effects functions.
dep.notify()
return true // In strict mode, a return value is required}})}Copy the code
The test code
// Call reactive to make objects reactive
const obj = reactive({
name: 'aaa'.age: 18
})
const obj1 = reactive({
name: "bbb".age: 19
})
// Use watchEffect to implement data changes and the function is re-executed
watchEffect(() = > {
console.log(obj.name);
})
watchEffect(() = > {
console.log(obj1.name);
})
obj.name = 'ccc'
obj1.name = 'ddd'
// The code first prints 'aaa' 'BBB'
// Since we have remodified obj.name and obj1.name, we will re-execute the side effect function, printing 'CCC' and 'DDD'.
Copy the code
conclusion
We use a reactive function on an object so that we can listen for its properties to get and change. The watchEffect function is executed by saving the function to a global variable effect before executing it, and then executing it on the function passed in. In the process of executing this function, we use a property of a responsive object, so we go to the get method of that property. In the GET method, we get the DEP instance by target and key, and then we call the addDep method of that DEP instance, The addDep method collects the function we saved in the global effect variable into the dependency. This allows us to collect dependencies by executing the watchEffect function passed in. When we modify a property of a reactive object, we come to the property’s set method. In the set method, we modify the object properties first so that we can ensure that the re-executed side effect function gets the new value. We then get the DEP instance through the getDep method and call notify of the DEP instance, thus achieving responsiveness.