Sword finger Vue3| thoroughly understand Vue3 responsive principle (proxy,Reflect)

Posted by misseether on Sun, 06 Mar 2022 09:59:05 +0100

What is responsive?

catalogue

Function encapsulation

Class encapsulation

Listening object change

let m = 3

console.log(m);
console.log(m +'1aaaaaa');
console.log('hello');

When the value of m changes, I hope the following three lines of code will be executed again automatically.

// obj
const obj = {
  name:'Sherry',
  age:18
}

console.log('hello');
console.log('i love u ');
console.log(obj.name);

Requirement: when the value of name changes, the next three lines of code will be executed again. We first want to encapsulate a function

Function encapsulation

const obj = {
  name:'Sherry',
  age:18
}


// 
let reactiveFn = []
function watchFn(fn){
  reactiveFn.push(fn)

}

watchFn(function(){
  console.log('hello')
  console.log('i love u ')
  console.log(obj.name)
  }
 
)
obj.name = 'Lily'

reactiveFn.forEach(fn =>{
  fn()
})

The array stores the functions we want to execute. When we want to execute any function when the value changes, we put the function into watchFn. watchFn will push the function in and finally call forEach. But now this array collects all the functions that need to be executed when the name changes. If it is age, it won't work. It's inconvenient to change an object, so we don't use the array

Class encapsulation

class Depend{
  constructor(){
    this.reactiveFn = []
  }
  addDepend(reactiveFn){
    this.reactiveFn.push(reactiveFn)
  }
}
// obj
const obj = {
  name:'Sherry',
  age:18
}

const depend = new Depend()
function watchFn(fn){
 depend.addDepend(fn)

}

watchFn(function(){
  console.log('hello')
  console.log('i love u ')
  console.log(obj.name)
  }
 
)
obj.name = 'Lily'

reactiveFn.forEach(fn =>{
  fn()
})

At this time, after encapsulating the array and the method added to the array into the class, age and name will be separated. Each attribute will have a corresponding array. Now one attribute can correspond to one dep object

Now, we can't change a property. We'll execute it after forEach every time. Let's put another method in the class

  notify(){
    this.reactiveFn.forEach(fn => {
      fn()
    })
  }

In this way, notify is called when responding

Listening object change

It hasn't been done yet. We should let the object execute itself after we change the attribute value of the object, instead of manually calling the function to execute it. Now we should listen to the changes of object attributes

Two methods: Proxy(vue3), object Define property (vue2). Let's talk about Proxy

const obj = {
  name:'Sherry',
  age:18
}
// Monitor obj changes
const objProxy = new Proxy(obj,{
  get:function(target,key,receiver){
    return Reflect.get(target,key,receiver)
  },
  set:function(target,key,newValue,receiver){
   Reflect.set(target,key,newValue,receiver)
    // We want automatic monitoring
    depend.notify()

  }
})

function watchFn(fn){
 depend.addDepend(fn)
}


const depend = new Depend()

watchFn(function(){
  console.log('hello')
  console.log('i love u ')
  console.log(objProxy.name)
  }
 
)
objProxy.name = 'Lily'
objProxy.name = 'Lily'
objProxy.name = 'Lily'

Now, whenever we change the name, the Proxy will automatically listen for us and execute notify

 

But if age changes


const depend = new Depend()

watchFn(function(){
  console.log('hello')
  console.log('i love u ')
  console.log(objProxy.name)
  }
 
)
watchFn(function(){
  console.log(objProxy.age,'age is change111');
})
watchFn(function(){
  console.log(objProxy.age,'age is change22');
})


objProxy.age = 100

When the age changes, all the functions in watchFn will execute by themselves

In the current design, since the depend in the set function collects the dependencies of all attributes, all functions will be re executed no matter which attribute changes. This is not possible. We need to make a distinction

// Encapsulate a function to get depend ent
const targetMap = new WeakMap()
function getDepend(target,key){
  // Get object according to target
  let map = targetMap.get(target)
  // New map
  if(!map){
    map = new Map()
    targetMap.set(target, map)
  }
  // Get depend ent according to key
  let depend = map.get(key)
  if(!depend){
    depend = new Depend()
    map.set(key,depend)
  }
  return depend
}
// obj response
const obj = {
  name: 'Sherry',
  age: 18
}
// Monitor obj changes
const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set: function (target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // We want automatic monitoring
    // depend.notify()
    // Get the most authentic depend ent, name/age
    const depend = getDepend(target,key)
    depend.notify()
  }
})

Let's take a brief look at the relationship diagram. This diagram can well understand the encapsulated function getappend. As for why it is map, the difference between map and object is that the key name of map can be any value, and the object can also be, but the object can't be. The key name of the object can't be an object. If it is an object, it will be automatically converted into a string. If you are interested, you can test it

 

But now let's hit dependency discovery

function getDepend(target,key){
  // Get object according to target
  let map = targetMap.get(target)
  // New map
  if(!map){
    map = new Map()
    targetMap.set(target, map)
  }
  // Get depend ent according to key
  let depend = map.get(key)
  if(!depend){
    depend = new Depend()
    console.log(depend.reactiveFn);
    map.set(key,depend)
  }
  return depend
}


objProxy.name = 'Lily'


objProxy.age = 100

It's empty

When should we collect dependencies?

When you get

const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
    // I want to collect dependencies here
    const depend = getDepend(target, key)
    depend.addDepend()
    return Reflect.get(target, key, receiver)
  },

watchFn(function () {
  console.log('hello')
  console.log('i love u ')
  console.log(objProxy.name)
}

Can't get watchFn in adddepot

Using global variables

let activeReactiveFn = null 
function watchFn(fn) {
  activeReactiveFn = fn 
  depend.addDepend(fn)
}

  get: function (target, key, receiver) {
    // I want to collect dependencies here
    const depend = getDepend(target, key)
    depend.addDepend(activeReactiveFn)
    return Reflect.get(target, key, receiver)
  },

Optimize: don't want get to care about whether there is a value in the global variable activeReactiveFn. Pull it out

let activeReactiveFn = null 

class Depend {
  constructor() {
    this.reactiveFn = []
  }
  addDepend(reactiveFn) {
    this.reactiveFn.push(reactiveFn)
  }

  notify() {
    this.reactiveFn.forEach(fn => {
      fn()
    })
  }
  // optimization
  depend(){
    if(activeReactiveFn){
      this.reactiveFn.push(activeReactiveFn)
    }
  }
}

In this way, when get listens, you only need to call the depend ent method

For the same WatchFn, we don't care how many times the attribute is accessed. In general, we only need to execute this function once

class Depend {
  constructor() {
    this.reactiveFn = new Set()
  }
  addDepend(reactiveFn) {
    this.reactiveFn.add(reactiveFn)
  }

  notify() {
    this.reactiveFn.forEach(fn => {
      fn()
    })
  }
  // optimization
  depend(){
    if(activeReactiveFn){
      this.reactiveFn.add(activeReactiveFn)
    }
  }
}

Topics: Javascript Front-end Vue.js