The responsive principle in Vue3, why use Proxy (proxy) and Reflect (reflection)?

Foreword:

        Vue3 has been out for a long time. I believe that many of you are already using Vue3 for production, but how is Vue3 better than Vue2? Today, let’s learn more about the responsiveness principle of Vue3 . By the way, let’s talk about  how the responsiveness of Vue3 is better than that of Vue2 . The space is a bit long, let’s savor it together!

Table of contents

Review the responsive principle of Vue2

1. Basic use

2. Listen to multiple properties on the object

3. In-depth monitoring of an object

4. Monitor array

Learn the responsive principle of Vue3

1. Basic use

2. Solve the problems encountered in Vue2 Object.defineProperty

3. Is the two-way binding of Vue3 really written like this? 

What is Reflect?

4. Why Proxy should be used together with Reflect

① When triggering the hijacking of the proxy object, ensure the correct this context pointing

② Framework robustness

Conclusion:


Review the responsive principle of Vue2


1. Basic use

Syntax: Object.defineProerty(obj, prop, descriptor)

Function : define a new property in an object, or modify an existing property of an object, and return an object

Parameters :

  1. the object to add the attribute to

  2. The name of the property to define or modify or [ Symbol]

  3. the property descriptor to define or modify

see a simple example

<script type="text/javaScript">
  let person = {}
  let personName = 'Barry'

  // 在 person 对象上添加属性 nameB,值为personName
  Object.defineProperty(person,'nameB',{
      //但是默认是不可枚举的(for in打印打印不出来),可:enumerable: true
      //默认不可以修改,可:wirtable:true
      //默认不可以删除,可:configurable:true

      get(){
          console.log('tigger get');
          return personName
      },
      set(val){
          console.log('trigger set');
          personName = val
      },
  })

  //当读取person对象的nameB属性时,触发get方法
  console.log(person.nameB)

  //当修改personName时,重新访问person.nameB发现修改成功
  personName = 'liming'
  console.log(person.nameB)

  // 对person.namep进行修改,触发set方法
  person.nameB = 'huahua'
  console.log(person.nameB)
</script>

In this way, we successfully listened to changes in the name attribute on the person.

2. Listen to multiple properties on the object

In the above usage, we only listen to the change of one property, but in reality, we usually need to monitor the change of multiple properties at a time.
At this time, we need to cooperate with Object.keys(obj) to traverse. This method can return a character array of all enumerable properties on the obj object. (In fact, for in traversal can also be used)

let person = {
    name:'Barry',
    age:22
}

console.log(Object.keys(person)); 
// ['name', 'age']

According to the above API, we can traverse all the properties on the hijacked object, but we found that the effect was not achieved. The following is a wrong version written:

let person = {
    name:'Barry',
    age:22
}

console.log(Object.keys(person));
/**
 * Object.defineProperty 复杂使用
*/

Object.keys(person).forEach(key=>{
    Object.defineProperty(person,key,{
        get(){
            return person[key]
        },
        set(val){
            console.log(`modify person object ${key}`);
            person[key] = val
        }
    })
})

console.log(person.age);

 It seems that there is no problem with writing, but try to run it, and you will find the same error as I reported --- stack overflow  

Why is this?

  Let's focus on the get method. When we access the attributes of the person, the get method will be triggered and the person[key] will be returned. However, accessing the person[key] will also trigger the get method, resulting in recursive calls and eventually stack overflow. 

This also leads to our following method, we need to set up a transit Observer , so that the return value in get does not directly access obj[key]

let person = {
    name:'Barry',
    age:22
}

console.log(Object.keys(person));

/**
 * Object.defineProperty 复杂使用(正确版本)
 */

// 实现一个响应式函数
function defineProperty(obj,key,val) {
    Object.defineProperty(obj,key,{
        get(){
            console.log(`trigger ${key} property`);
            return val
        },
        set(newVal){
            console.log(`${key} set property ${newVal}`);
            val = newVal
        }
    })
}

// 实现一个遍历函数 Observer
function observer(obj){
    Object.keys(obj).forEach(key=>{
        defineProperty(obj,key,obj[key])
    })
}

observer(person);
console.log(person.name);
person.age = 30;
console.log(person.age);

3. In-depth monitoring of an object

So how do we solve the situation where an object is nested within an object? In fact, you can add a recursion to the above code, and then use recursion to achieve it easily .

We can observe that Observer is actually the monitoring function we want to implement. Our expected goal is : as long as the object is passed into it, the property monitoring of this object can be realized, even if the property of the object is also an object

The specific code is as follows:

function defineProperty(obj,key,val) {
    if(typeof val === 'object'){
        observer(val)
    }
    Object.defineProperty(obj,key,{
        get(){
            console.log(`trigger ${key} property`);
            return val
        },
        set(newVal){
            console.log(`${key} set property ${newVal}`);
            val = newVal
        }
    })
}

Of course, we also need to add a recursive stop condition in the observer :

function observer(obj){
    if(typeof obj !== 'object' || obj === null){
        return
    }
    Object.keys(obj).forEach(key=>{
        defineProperty(obj,key,obj[key])
    })
}

In fact, it is almost solved here, but there is still a small problem . If a property is modified, if the original property value is a string, but we reassign an object, how do we monitor the newly added object? What about all properties? In fact, it is also very simple, just modify the set function:

set(newVal){
    console.log(`${key} set property ${newVal}`);
    if(typeof val === 'object'){
        observer(key)
    }
    val = newVal
}

4. Monitor array

So what if the object's property is an array? How do we implement monitoring?

Please see the following piece of code:

let hobby = ['抽烟','喝酒','烫头']
    let person = {
        name:'Barry',
        age:22
    }


// 把 hobby 作为 person 属性监听
Object.defineProperty(person,'hobby',{
    get(){
        console.log('tigger get');
        return hobby
    },
    set(newVal){
        console.log('tigger set',newVal);
        hobby = newVal
    }
})

console.log(person.hobby);
person.hobby = ['看书','游泳','听歌']
person.hobby.push('游泳')

We found that  the set method cannot monitorpush the elements added to the array through the method .

In fact, accessing or modifying the existing elements in the array through the index can trigger get and set , but for the elements added through push and unshift, an index will be added. In this case, manual initialization is required , and the newly added elements can be was monitored. In addition, deleting an element through pop or shift will delete and update the index, and also trigger the setter and getter methods.

In Vue2.x, this problem is solved by rewriting the method on the Array prototype, so I won’t talk about it here, and uu who are interested can learn more~

What are the methods that Vue can monitor the changes of the array? Why can these methods be monitored? https://cn.vuejs.org/v2/guide/list.html#%E6%95%B0%E7%BB%84% E6%9B%B4%E6%96%B0%E6%A3%80%E6%B5%8B Vue source code

Learn the responsive principle of Vue3


Does it feel a bit complicated? In fact, in the above description, we still have a problem that has not been solved : that is, when we want to add a new property to the object , we also need to manually monitor this new property.

It is also for this reason that when using vue to add properties to arrays or objects in data, you need to use vm.$set to ensure that the new properties are also responsive.

It can be seen that data monitoring through Object.definePorperty () is cumbersome and requires a lot of manual processing . This is why You Yuxi switched to Proxy in Vue3.0. Next, let's take a look at how Proxy solves these problems!


1. Basic use

Syntax : const p = new Proxy( target, handler );

Parameters :

  1. target: The target object to use  Proxy the wrapper on (can be any type of object, including native arrays, functions, or even another proxy)

  2. handler: An object that usually has functions as attributes, and the functions in each attribute define the  p behavior of the agent when performing various operations.

Through Proxy , we can 设置代理的对象intercept some operations on the object, and all kinds of operations on this object from the outside world must first pass through this layer of interception. (similar to defineProperty )

Let's look at a simple example together

<script type="text/javaScript">
    // 定义一个需要代理的对象
    let person = {
        name:'Barry',
        age:22
    }

    let p = new Proxy(person,{
        get(target,key){
            return target[key]
        },
        set(target,key,val){
            return target[key] = val;
        }
    })

    console.log(p);

    //测试 get 是否可以拦截成功
    console.log(p.name); // 输出 Barry
    console.log(p.age);  // 输出 22
    console.log(p.job);  // 输出 undefined

    //测试 set 是否可以拦截成功
    p.age = 18
    console.log(p.age);
  </script>

 It can be seen that Proxy proxies the entire object , not a specific property of the object , and does not require us to perform data binding one by one through traversal .

It is worth noting that: after we used Object.defineProperty() to add a property to the object, our read and write operations on the object property are still in the object itself .
But once Proxy is used, if we want the read and write operations to take effect, we need to operate on the instance object of Proxy  .

2. Solve the problems encountered in Vue2 Object.defineProperty

When using Object.defineProperty above, the problems we encountered were:

1. Only one property can be monitored at a time, and all properties need to be traversed to monitor. We have already solved this above.
2. When the property of an object is still an object, recursive monitoring is required.
3. For the new properties of the object, it needs to be monitored manually
4. For the elements added by the push and unshift methods of the array, it is also impossible to monitor

These problems are easily solved in Proxy, let's look at the following code.

Let's check the second question together

<script type="text/javaScript">
  // 定义一个需要代理的对象
  let person = {
      name:'Barry',
      age:22,
      job:{
          city:'ShenZhen',
          salary:50
      }
  }

  let p = new Proxy(person,{
      get(target,key){
          return target[key]
      },
      set(target,key,val){
         return target[key] = val;
      }
  })

  console.log(p);

  //测试 get
  console.log(p.job);
  console.log(p.job.city);
  console.log(p.job.salary);

  //测试 set
  p.job.salary = 60
  console.log(p.job);
</script>

It can be seen that the job object in the person object has been successfully monitored, and all attributes of the job can be successfully monitored

Check the third question together

console.log(p);

//测试 get
console.log(p.job);
console.log(p.job.type);

//测试 set
p.job.type = 'Web'
console.log(p.job);

With these few lines of code, it can be successfully tested. The accessed p.job.type is an attribute that does not exist on the original object, but when we access it, it can still be intercepted by get.

Check the fourth question together 

<script type="text/javaScript">
  let hobby = ['学习','看书','听歌'];

  let h = new Proxy(hobby,{
      get(target,key){
          return target[key]
      },
      set(target,key,val){
          return target[key] = val
      }
  })


  //检验 get 和 set
  console.log(h) // 输出 Proxy {0: '学习', 1: '看书', 2: '听歌'}
  console.log(h[0]) // '学习'

  h[0] = '游泳';

  console.log(h); // Proxy {0: '游泳', 1: '看书', 2: '听歌'}

  // 检验push增加的元素能否被监听

  h.push('爬山')
  console.log(h); // 输出 Proxy {0: '游泳', 1: '看书', 2: '听歌', 3: '爬山'}
</script>

Here, we have perfectly solved the problems encountered by Vue2! Our analysis of Proxy here is not very comprehensive. Careful students may find that there is an additional parameter receiver in the get trap in Proxy after reading the MDN document of Proxy. Friends who are interested can go to check some documents, and I won’t introduce them in detail here.

3. Is the two-way binding of Vue3 really written like this? 

In fact, this is not the case. The responsiveness of Vue3 is designed through Proxy (proxy) and Reflect (reflection). Why should it be designed this way? let's look down

Basic usage of Proxy && Reflect

<script type="text/javaScript">
  let person = {
      name:'Barry',
      age:22,
      job:{
          city:'ShenZhen',
          salary:30
      }
  }

  let handler = {
      get(target,key,receiver){
          return Reflect.get(target,key,receiver)
      },
      set(target,key,val){
          Reflect.set(target,key,val)
      },
      deleteProperty(target,key){
          return Reflect.deleteProperty(target,key)
      }
  }

  let p = new Proxy(person,handler);

  console.log(p.name); // 输出 Barry
  console.log(p.job.salary); // 输出 30

  p.job.salary = 50;
  console.log(p.job.salary); // 输出 50

  delete p.job
  console.log(p); // 输出Proxy {name: 'Barry', age: 22}
</script>

The above is just our rough implementation. Thinking that some unfamiliar friends may ask:

What is Reflect?

In fact, like Proxy, Reflect is an advanced API of ES6. Reflect is also a built-in class of window, which can be accessed through window.Reflect, see the figure below 

 

4. Why Proxy should be used together with Reflect

① When triggering the hijacking of the proxy object, ensure the correct this context pointing

Everything in the above Demo looks smooth, right? Careful students may find that there is an additional parameter receiver in the get trap in Proxy after reading the MDN document of Proxy. 

So what does the receiver here mean? Most students will understand it as a proxy object

<script type="text/javaScript">
  const person = {
      name:'Barry',
      age:22
  }

  const p  =new Proxy(person,{
        // get陷阱中target表示原对象 key表示访问的属性名
        get(target, key, receiver) {
            console.log(receiver === p);
            return target[key];
        },

  })
</script>

In the above example, we received the receiver parameter on the get trap of the Proxy instance object .

At the same time, if we print it inside the trap, console.log(receiver === proxy);it will print true, indicating that the receiver is indeed equal to the proxy object.

Then you can think about what the receiver here is? In fact, this is also the meaning of the existence of the third receiver in get in proxy.

It is to pass the correct caller pointing to

Through our printing of window.Reflect above, we can see that the methods and attributes of Reflect are the same as those of Proxy, so Reflect get also has the third  receiver attribute;

<script type="text/javaScript">
  const person = {
      name:'Barry',
      age:22
  }

  const p  =new Proxy(person,{
        // get陷阱中target表示原对象 key表示访问的属性名
        get(target, key, receiver) {
            console.log(receiver === p);
            return Reflect.get(target,key,receiver)
        },

  })
  console.log(p.name);
</script>

The principle of the above code is actually very simple:

In the third parameter of the get trap in Reflect, we pass the receiver in Proxy, which is obj, as a formal parameter, which will modify the this point when calling. 

You can simply  Reflect.get(target, key, receiver) understand it as , but this is a piece of pseudocode, but you may understand it better this way. target[key].call(receiver)

I believe that you have understood what the receiver in Relfect means after reading this. Yes, it can modify the this point in the property access to the incoming receiver object.

 

 

② Framework robustness

Why do you talk about the robustness of the framework? Let's look at a piece of code together

<script type="text/javaScript">
  const person = {
      name:'Barry',
      age:22
  }


  Object.defineProperty(person,'height',{
      get(){
          return 180
      }
  })
  Object.defineProperty(person,'height',{
      get(){
          return 170
      }
  })
</script>

Look at the browser operating environment

We can see that using Object.defineProperty() to repeatedly declare an error , because JavaScript is a single-threaded language, once an exception is thrown, any subsequent logic will not be executed, so in order to avoid this situation, we use the underlying It is not elegant enough to write a lot of try catches to avoid.

Let's take a look at what happens to Reflect?

<script type="text/javaScript">
   const person = {
       name:'Barry',
       age:22
   }


  const h1 = Reflect.defineProperty(person,'height',{
       get(){
           return 180
       }
   })
   const h2 =  Reflect.defineProperty(person,'height',{
       get(){
           return 175
       }
   })
   console.log(h1); // true
   console.log(h2); // false
   console.log(person); //age: 22,name: "Barry",height: 180
</script>

 

We can see that using Reflect.defineProperty() has a return value , so use the return value to judge whether your current operation is successful.

Conclusion:

This is the end of the article. Maybe you are still ignorant in many places. You might as well turn on the computer, knock together, and try it. It may be very helpful;

Secondly, the explanations in many places in the article are not detailed enough, so it will also lead to your understanding of the whole is not particularly clear, I will try to improve it as soon as possible;

Finally, thank you to everyone who watched, cheers together~~~

 

Guess you like

Origin blog.csdn.net/weixin_56650035/article/details/124894158