Analysis of response principle of vue

Ask yourself more why every day. You always have unimaginable gains. A rookie Xiaobai's growth path (copyer)


Decide whether to continue reading this note

This is a purely personal learning step, the process of practicing code from shallow to incremental, and the idea is improved step by step

If you don't know anything about the response, you can try to look at the notes here, which is applicable to Xiaobai (because I am Xiaobai)

If you know more about the response, you don't have to look at it. There's little gain here


What is responsive?

A simple understanding is that a function depends on a variable. When the variable is changed, the function will be automatically re executed and updated.

const info = { count: 0}

function doubleCount() {     //This function uses info count
    console.log(info.count * 2)
}

getCount()

//When info When the count is changed, you want to automatically execute doubleCount(), which faces info Count has dependency
info.count++

When multiple functions depend on a variable, multiple functions need to be executed multiple times. Therefore, a single function is encapsulated to uniformly execute these functions.

//Suppose there is another function, which also depends on info Count
function powCount() {
    console.log(info.count * info.count)
}

//When the count is changed, doubleCount and powCount need to be executed automatically

Simply encapsulate the implementation of Dep class (manual collection version)

class Dep {
    constructor() {
        this.subscribers = new Set()    //Ensure that the collected dependencies are not duplicated
    }
    
    //Collect side effects
    //Side effects: when the variable changes, other functions also need to be executed. These are the side effects produced by the variable
    addEffect(effect) {
        this.subscribers.add(effect)
    }
    
    //Variable change, notify execution
    notify() {
        this.subscribers.forEach(item => {
            item()
        })
    }
}

Use in instances:

const dep = new Dep()  //Instantiation dependency

const info = { count: 0}

function doubleCount() {     //This function uses info count
    console.log(info.count * 2)
}
function powCount() {
    console.log(info.count * info.count)
}

//Collection dependency
dep.addEffect(doubleCount)
dep.addEffect(powCount)

info.count++

//Variable change, re execute side effects
dep.notify()

Refactoring the implementation of encapsulated Dep class (automatic collection version)

Here, the depend ent function is used instead of the addEffect function

addEffect: we need to add dependencies manually

Dependent: it is used to judge whether there are side effects. If there are side effects, they will be added, so there is no need to collect them manually

class Dep {
    constructor() {
        this.subscribers = new Set()
    }

    depend() {    //Automatic collection of dependent functions
        if(activeEffect) {  
            this.subscribers.add(activeEffect)
        }   
    }

    notify() {
        this.subscribers.forEach(item => {
            item()
        })
    }
}

//Encapsulate a watchEffect function to automatically collect

let activeEffect = null   //Determine whether there are side effects
function watchEffect(effect) {
    activeEffect = effect      //Assign side effects and add
    dep.depend(activeEffect)   //Collection add
    effect()                   //First execution
    activeEffect =null         //Reset empty
}

use:

const info = { count: 100}

const dep = new Dep()

watchEffect(function() {    //The functions defined here use watchEffect, which will automatically collect dependencies
    console.log(info.count * 2);
})

watchEffect(function () { 
    console.log(info.count * info.count);
})

info.count++

dep.notify()

Responsive implementation of reactive (data hijacking: get())

The process is difficult. If you understand the basic ideas above, let's look at the logic below!

Let's start with a code:

const dep = new Dep()
const info = { count: 100, name: 'james'}
const bar = { age: 19 }

watchEffect(function() {   //watch1
    console.log(info.count, info.name)
})
watchEffect(function() {   //watch2
    console.log(info.name)
})
watchEffect(function() {   //watch3
    console.log(bar.age)
})

watchEffect collection dependency

watch1 depends on info count info. name

watch2 depends on info name`

watch3 depends on bar age

We know that the depand function collects different side effects and adds them to subscribers. As long as the dependency changes, all side effects will be executed. This is definitely a waste of performance, not what we expect. So how should we solve it?

We need to use another data structure: Map and WeakMap

Objective: to create multiple dep instance objects and use subscribers to save the side effects of different data


1. First, the structure is analyzed

For the above, if there are two objects info (with two attributes) and bar (with one attribute), you should instantiate three dep objects and use the subscribers in instantiation to save their corresponding side effects respectively.

//For the info object, you need to create two dep objects dep1 and dep2
info.count:  dep1.subscribers = [watch1]
info.name:   dep2.subsrcibers = [watch1, wathc2]

//For the bar object, you need to create a dep object dep3
bar.age:      dep3.subsrcibers = [watch3]

When the properties of info and bar change, execute the side effects collected by their respective subscribers, and then execute the notify function

Tips:

Understanding: the difference between WeakMap and Map

Similarities:

They are all key: value pairs

difference:

The key value of Map is string, and value is any type

The key value of WeakMap is: the object value is of any type

After understanding the difference between the above WeakMap and Map, design the Map structure for the code

Pseudo code data structure:

//There are two objects above, so use weakMap to save them
const targetMap = new WeakMap()

//Info object, key is info object, and value is a map
const infoMap = new Map(info)        //Create a map
targetMap[info] = infoMap
//The value value is a map, so it exists in the form of a key value pair. The key is the attribute name of the info object
infoMap['count'] = dep1.subscribers
infoMap['name'] = dep2.subscribers

//bar object
targetMap[bar] = new Map(bar)
//It's the same as the above explanation

Therefore, according to the above analysis, we can know exactly which data changes and execute the corresponding dep subscribers

The code implements the above data structure

//Create a WeakMap to save info and bar objects (the info object is mainly used as an example below)
const targetWeakMap = new WeakMap()

//getDep(info, 'name') gets dep whose attribute value of info is name
function getDep(target, key) {
    //Get the value where the key in the WeakMap is the info object
    let depsMap = targetWeakMap.get(target)
    //During initialization, there is no, so new Map() is used as value and re assigned
    if(!depsMap) {   
        depsMap = new Map()   
        targetWeakMap.set(target, depsMap)
    }
	
    //According to the info object, get the value, which is Map. According to the key, you can get the corresponding dep
    let dep = depsMap.get(key)
    //Handle initialization. If not, create a dep instance object
    if(!dep) {
        dep = new Dep()
        depsMap.set(key, dep)
    }
	
    //Return the dep obtained
    return dep
}

After analyzing the above code, you can get dep, and then you can write reactive responsive logic


2. reactive implementation of vue2

const info = reactive({count: 100, name: 'james'})

Objective: call reactive, pass an object and return a responsive object

//Raw: raw data
function reactive(raw) {
    //Traverse to get all key values
    Object.keys(raw).forEach(item => {
        const dep = getDep(raw, item)
        let value = raw[item]
        Object.defineProperty(raw, item, {
            //When you get the attribute value in the info object, you call the get method. Then add dependencies here (the so-called data hijacking)
            get() {
                dep.depend()
                return value
            },
            set(newValue) { //When the value is set, notify is executed and all side effects are executed
                if(value !== newValue) {
                    value = newValue
                    dep.notify()
                }
            }
        })
    })
    return raw
}

3. Explain the process implementation of the complete code

//Step 1:
class Dep {
    constructor() {
        this.subscribers = new Set()
    }
    depend() {
        if(activeEffect) {
            this.subscribers.add(activeEffect)
        }   
    }
    notify() {
        this.subscribers.forEach(item => {
            item()
        })
    }
}

//Step 2:
let activeEffect = null
function watchEffect(effect) {
    activeEffect = effect
    effect()
    activeEffect =null
}

//Step 3:
const targetWeakMap = new WeakMap()
function getDep(target, key) {
    //Get the Map of dep according to the target object
    let depsMap = targetWeakMap.get(target)
    if(!depsMap) {
        depsMap = new Map()
        targetWeakMap.set(target, depsMap)
    }

    //According to the obtained depsMap, get their dep through the key value
    let dep = depsMap.get(key)
    if(!dep) {
        dep = new Dep()
        depsMap.set(key, dep)
    }

    return dep
}

//Raw: raw data
function reactive(raw) {
    Object.keys(raw).forEach(item => {
        const dep = getDep(raw, item)
        let value = raw[item]
        Object.defineProperty(raw, item, {
            get() {
                dep.depend()
                return value
            },
            set(newValue) {
                if(value !== newValue) {
                    value = newValue
                    dep.notify()
                }
            }
        })
    })
    return raw
}

//Test code
const info = reactive({ count: 100, name: 'james'})
const bar = reactive({age: 12})

watchEffect(function() {   //watch1
    console.log('watch1: ', info.count, info.name)
})
watchEffect(function() {   //watch2
    console.log('watch2: ', info.name)
})
watchEffect(function() {   //watch3
    console.log('watch3: ', bar.age)
})

info.count++

Code interpretation:

  • Step 1: define the Dep class, which implements two methods: depend (collect side effect function) and notify (re execute side effect function after data change)

  • Step 2: define the watchEffect function, execute it for the first time, and collect dependencies

  • Step 3: define a reactive function to change the data into responsive data

  • Step 4: test code verification

    watchEffect(function() {   //watch1
        console.log('watch1: ', info.count, info.name)
    })
    //Info. Is used directly here Count, object will be called directly The get method of defineproperty, then the dependency will be collected
    
    info.count++
    //If you change the data, you will directly call object The set method of defineproperty will execute the notify function
    
  • Step 5: verify the results

    //Run in node environment
    
    //First execution
    watch1:  100 james
    watch2:  james
    watch3:  12
    
    //info. Execute after count change
    watch1:  101 james
    

4. reactive implementation of vue3

proxy object used by vue3.

Why vue3 choose Proxy?
  • Object. When defineproperty hijacks the properties of an object, if an element is added:

    • Then vue2 you need to execute defineProperty again, such as Vue$ Set is re execution
    • The Proxy hijacks the whole object without special processing
  • Modify different objects

    • When using defineProperty, we can trigger interception by modifying the original obj object

    • Proxy, you must modify the proxy object, that is, the instance of proxy, before it can be triggered

      //vue2
      function reactive(raw) {
          return raw
      }
      //vue3
      function reactive(raw) {
          return new Proxy(raw, {})
      }
          
      
  • Proxy can observe more types than defineProperty

    • Catcher for has: in operator
    • deleteProperty: the catcher for the delete operator
    • Other operations
  • Disadvantages: Proxy is not compatible with IE

code implementation

function reactive(raw) {
    return new Proxy(raw, {
        get(target, key) {
            const dep = getDep(target, key)
            dep.depend()
            return target[key]
        },
        set(target, key, newValue) {
            const dep = getDep(target, key)
            target[key] = newValue
            dep.notify()
        }
    })
}

Summary:

Know a lot and gain a lot. Continue to refuel. If you are wrong, please spray.

Keywords: Vue

Added by Paul Ferrie on Thu, 23 Dec 2021 11:19:14 +0200