Vue3 responsive principle

Posted by kirk112 on Wed, 05 Jan 2022 07:57:08 +0100

vue3 responsive principle

If there are any errors, please point them out~

More study notes request stamp: https://github.com/6fa/WebKno...

1. Responsive core

Suppose you want sum to be a responsive variable in the following example:

let num1 = 1;
let num2 = 2;
let sum = num1 + num2;

num1 = 10
console.log(sum) //sum is still 3, non responsive

The parts to be realized are:

  • Data hijacking: know when num1 and num2 change
  • Dependency collection: know what data sum depends on. In the example, sum depends on num1 and num2, so establish their dependencies
  • Dispatch update: when the dependent data num1 and num2 change, notify the response object sum to recalculate

vue3 intercepts data reading and setting (data hijacking) through the Proxy. When data is read, it triggers dependent collection through the track function; when data is set, it sends updates through the trigger function.

So vue3 how do you use a response?

  • vue3 can not only return a responsive object through the data function, but also create a responsive variable through ref and reactive. When using reactive, etc., the data is internally wrapped with Proxy.
  • When using computed, watch, view rendering functions, etc., it can be regarded as declaring a callback that depends on responsive data. This callback will be passed into effect (side effect function). When the dependent data changes, the callback will be called again, so that computed, etc. will be updated.

To realize the simple version of the response formula, its approximate structure is as follows:

//Create a responsive variable to intercept the get and set of data
function reactive(obj){}

//The effect function wraps functions that rely on responsive data cb
//When the data that cb depends on is updated, execute effect again
function effect(cb){}

//Rely on collection to establish the mapping relationship between responsive data and effect
function track(target, property){}
//Trigger the update and execute the effect function according to the dependency
function trigger(target, property){}

use:

let obj = reactive({
  num1: 10,
  num2: 20
})
let sum = 0

effect(()=>{
  sum = obj.num1 + obj.num2
})

console.log(sum) //30

obj.num1 = 100
console.log(sum)  //Should be 120

2. Basic use of proxy & reflect

Before creating a responsive variable, you need to know the basic use of Proxy and Reflect.

JS is difficult to track a single local variable, but it can track the property changes of objects: Proxy and Reflect of ES6 used by vue3.

  • Proxy intercepts operations such as object reading and setting, and then performs operation processing. However, the source object is not manipulated directly, but through the proxy object of the object.

    //Proxy usage
    //A Proxy object consists of a target (target object) and a handler (object that specifies the behavior of the Proxy object)
    
    let target = {
    a:1,
    b:2
    }
    let handler = {
    //receiver refers to the object that calls the behavior, usually the Proxy instance itself
    get(target, propKey, receiver){
    return target[propKey]    //Getters can not even return data
    },
    
    set(target, propKey, value, receiver){
    target[propKey] = value
    }
    }
    
    let proxy = new Proxy(target, handler)
    console.log(proxy.a)                 //1
    
    proxy.a = 3
    console.log(proxy.a)        //3
  • Reflect can directly call the internal methods of the object. Like Proxy, it has operations such as obtaining and setting.

    //Reflect usage
    
    let target = {
    get a(){return this.num1 + this.num2},
    set a(val){return this.num1 = val}
    }
    //receiver is an optional parameter
    let receiver = {
    num1:10,
    num2:20
    }
    
    //Reflect.get(target, propKey, receiver)
    //It is equivalent to directly operating get a() {} of target
    Reflect.get(target, 'a', receiver)  //30 this is bound to the receiver
    
    //Reflect.set(target, propKey, value, receiver)
    Reflect.set(target, 'a', 100, receiver) //100
  • The function of Reflect is mainly to solve the binding problem of this, and bind this to the proxy object instead of the target object: for example, Reflect Get (target, property, receiver) when getting the property, if the property specifies a getter, this of the getter will be bound to the receiver object.

    //Proxy problem
    const obj = {
    a: 10,
    get double(){
    return this.a*2
    }
    }
    
    const proxyobj = new Proxy(obj,{
    get(target, propKey, receiver){
    return target[propKey]
    }
    })
    let obj2 = {
    __proto__: proxyobj
    }
    obj2.a = 20
    obj2.double //The expected value is 40, but the actual value is 20, because this in the getter of double is bound to obj
    
    
    
    
    //Use Reflect to solve this binding problem
    const obj = {
    a: 10,
    get double(){
    return this.a*2
    }
    }
    
    const proxyobj = new Proxy(obj,{
    get(target, propKey, receiver){                                //The receiver here is obj2
    return Reflect.get(target, propKey, receiver)
    }
    })
    let obj2 = {
    __proto__: proxyobj
    }
    obj2.a = 20
    obj2.double  //40. Through the receiver of Refelct, this in get double() is bound to obj2

3. Implementation of reactive function

The implementation of the reactive function of creating a responsive variable mainly depends on the internal instantiation of a Proxy object:

function reactive(obj){
  const handler = { //Intercept the get and set of data for processing
    get(){},
    set(){}
  }
  const proxyObj = new Proxy(obj,handler)
  return proxyObj  //Returns a proxy object instance
}

When data is obtained, it is necessary to start dependent data collection (to be implemented by the track function); when data is set, it is necessary to trigger update (to be implemented by the trigger function):

function reactive(obj){
  const handler = {
    get(target, propKey, receiver){
      const val = Reflect.get(...arguments)  //Read data
      track(target, propKey)    //Dependency collection
      return val
    },
    set(target, propKey, newVal, receiver){
      const success = Reflect.set(...arguments)    //Set data and return true or false
      trigger(target, propKey)    //Trigger update
      return success
    }
  }
  const proxyObj = new Proxy(obj,handler)
  return proxyObj  //Returns a proxy object instance
}

However, the above only works on one layer of objects, and does not work on multi-layer nested objects with attributes or objects. You need to manually recursively implement the response:

function reactive(obj){
  const handler = {
    get(target, propKey, receiver){
      const val = Reflect.get(...arguments)
      track(target, propKey)    
      if(typeof val === 'object'){
        return reactive(val)   //newly added
      }
      return val
    }
  }
  ...
}

4. Side effect function

Before implementing the above track and trigger, you should also understand the side effect functions. The side effect function effect is used to track the running functions, such as watch and computed. The code in the function will be passed into effect. When other data dependent on watch and computed changes, the code in the function will be re run.

Take vue3's computed as an example:

const num = ref(10) //num is responsive
const double = computed(()=>num*2)

The content in computed can be regarded as an update function that relies on responsive data (hereinafter referred to as the update function), and computed returns a ref reference. In the computed function, there will be operations similar to the following:

computed(cb){
  const result = ref()
  effect(()=>result.value = cb())
  return result
}

The update function is treated as a callback to the effect function:

//Side effect function currently running
let activeEffect = null

const effect = (cb)=>{
  activeEffect = cb
  //Run responsive function
  cb()
  activeEffect = null
}

If the effect function executes the update function, it will read the data it depends on. We have set up a proxy proxy for these data. At this time, the dependency collection is completed (establish the mapping relationship between the update function and the dependent data. When the data changes, the update function that depends on the data will be found through the mapping relationship and executed again).

Take the following example to illustrate:

let num = reactive({value: 10})
let double = 0
effect(()=>{double = num.value*2}) //double is responsive
let triple = num.value*3  //triple is not responsive

//1. num is wrapped into a reactive variable, which will intercept the acquisition and setting of its attributes
//2. Run the side effect function effect, and point the currently running side effect function activeEffect to the callback of effect, that is, the update function
//3. Execute activeEffect (update function)
//4. The update function will read num.value internally and trigger the dependency collection track function of proxy

//6. The track will establish a mapping relationship between num.value and the update function
//     The reason for establishing the mapping relationship is that when num.value changes, the trigger needs to find all update functions that depend on num.value
//   Then run it all again so that double is reassigned

//7. double is assigned
//8. Redirect the activeEffect to null

//8. Run to triple. Even if num.value is read, activeEffect is null at this time
//     The mapping relationship between num.value and activeEffect will not be established, so num.value will not be updated to triple when it is changed

5. Implementation of Track & trigger

track(target, property):

Mainly target Property is recorded together with the update function to form a mapping relationship, so that you know the dependency target What are the update functions for property

trigger(target, property):

Find the dependent target from the mapping relationship Property and rerun them

Rely on a target There can be many update functions for property. Use the Set structure to store them:

property1: Set [cb1,cb2,cb3...]

This set structure is called dep, and each responsive attribute must have a dep, which can be stored in the Map structure:

Map {
  property1: Set [cb1,cb2,cb3...],
  property2: Set [cb1,cb2,cb3...],
  property3: Set [cb1,cb2,cb3...],
  ......
}

This Map structure is called depsMap. But this is only an attribute in the same object. What if there are multiple objects?

Therefore, it is necessary to wrap another layer and wrap the Map of each object with another Map (called targetMap structure), but the targetMap uses the WeakMap structure, and the attributes of the WeakMap can only be objects:

WeakMap {
  obj1: Map { 
    property1:Set [cb1,cb2,cb3...],
    ...
  },
  obj2: Map { 
    property1:Set [cb1,cb2,cb3...],
    ...
  },
  ...
}

When the data is read, the track function uses this structure to map the responsive attribute and the update function that depends on it:

const targetMap = new WeakMap()

function track(target, property){
  if(!activeEffect)return 
  //Returns if activeEffect is null
  //activeEffect has a value only when effect() is run
  
  let depsMap = targetMap.get(target)
  //If the target object does not have a corresponding depsMap, create a new one
  if(!depsMap){
    targetMap.set(target, depsMap = new Map())
  }

  let dep = depsMap.get(property)
  //If the attribute does not have a corresponding dep, create a new one
  if(!dep){
    depsMap.set(property, dep = new Set())
  }

  dep.add(activeEffect) //Add the effect corresponding to the attribute into the mapping structure
}

When the data is set, the trigger function takes out all update functions corresponding to the data through the mapping structure and executes:

function trigger(target, property){
  const depsMap = targetMap.get(target)
  if(!depsMap) return
  
  const dep = depsMap.get(property)
  if(!dep) return
  //dep is a Set structure with forEach method
  dep.forEach((effect)=>{
    effect()
  })
}

6. Integration & use

Integrate the above code:

//reactive.js

//Implementation of effect function
let activeEffect = null
function effect(cb){
  activeEffect = cb
  cb()
  activeEffect = null
}

//Create a responsive variable function
function reactive(obj){
  const handler = {
    get(target, propKey, receiver){
      const val = Reflect.get(...arguments)//Read data
      track(target, propKey)  //Dependency collection
      if(typeof val === 'object'){
        return reactive(val)
      }
      return val
    },
    set(target, propKey, newVal, receiver){
      const success = Reflect.set(...arguments)//Set data and return true/false
      trigger(target, propKey)  //Trigger update 
      return success
    }
  }
  const proxyObj = new Proxy(obj,handler)
  return proxyObj
}

//Dependency collection function
const targetMap = new WeakMap() //Structure for storing mapping relationships
function track(target, property){
  if(!activeEffect)return 

  let depsMap = targetMap.get(target)
  //If the target object does not have a corresponding depsMap, create a new one
  if(!depsMap){
    targetMap.set(target, depsMap = new Map())
  }

  let dep = depsMap.get(property)
  //If the attribute does not have a corresponding dep, create a new one
  if(!dep){
    depsMap.set(property, dep = new Set())
  }

  dep.add(activeEffect) //Add the effect corresponding to the attribute into the mapping structure
}

//Dispatch update function
function trigger(target, property){
  const depsMap = targetMap.get(target)
  if(!depsMap) return
  
  const dep = depsMap.get(property)
  if(!dep) return
  dep.forEach((effect)=>{
    effect()
  })
}

Test use:

let obj = reactive({
  num1: 10,
  num2: 20,
  son:{
    num3:20
  },
})
let sum = 0

effect(()=>{
  sum = obj.num1 + obj.num2 + obj.son.num3
})

console.log(sum) //50

obj.num1 = 100
console.log(sum)  //130 it can be seen that sum is a response type

reference resources:

Principle and implementation of Vue3 responsive

Vue3 responsive principle + handwriting reactive

Vue3 responsive principle and implementation of reactive, effect and computed

ES6 Reflect and Proxy

Topics: Front-end Vue.js