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 manuallyb = a * 2This line of code.
  • How to makeb = a * 2Can this line of code be executed automatically? Want to makeb = a * 2This 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.definePropertyListening 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.

  • useObject.definePropertyIt 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 launchedProxyThis API, this API is used to implement listening objects, and this API is also effective for arrays. In the use ofProxy“, usually matchReflectUse 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 Vue3watchEffectFunction 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 classaddDepMethods.
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.