Related articles (Reference)

Vue responsive principles – Understand Observer, Dep, Watcher

In-depth responsive Principles – Vue official documentation

Vue3 is going to use Proxy as data driver, don’t you want to come in and have a look?

Vue3.0 — Discard Object. DefineProperty and explore Proxy based observer mechanism

Use Proxy to automatically add reactive properties

Explain the principle of Vue response

Do you really understand Vue’s data responsiveness

What is data responsiveness

The response

The Chinese word for “response” means “response”. For example, when someone calls you or sends you a message and you respond, the process is called response.

Data response

Vue’s official documentation clearly tells us:

  • Just modify the data data (render(data)The view is rerendered (no DOM manipulation required by the developer)
  • This linkage process is the data response of VUE
  • This is one of the most unique features of Vue — a non-invasive, responsive system.

How to understand “non-invasive”

Immutable:

  • That is, users cannot bypass Vue’s listening to tamper with internal data
  • Vue does this: “Whenever users modify internal data in data in any way, the current Vue component will listen and notify the corresponding Watcher to re-render the components associated with the data.”
  • Extension: This feature relies on “proxy” implementation

Analysis of Vue response type principle

Vue implements data responsiveness through Object.defineProperty

When we pass an Object as a data option to a Vue instance, Vue iterates through all the properties in the data, modifies them with Object.defineProperty, and monitors reads and writes of each property with getters/setters.

Each component instance corresponds to a Watcher instance that records the data dependencies used to render the view based on the firing of the getter. When the setter for this data fires (the data is modified), Watcher is notified so that the components associated with the data are re-rendered.

How to track change

  • When you pass an ordinary JavaScript object into a Vue instance as the data option, Vue iterates through all of the object’s properties, And use Object.defineProperty to turn all of these properties into getters/setters.
  • These getters/setters are invisible to the user, but internally they allow Vue to track dependencies and notify changes when the property is accessed and modified.
  • Each component instance corresponds to a Watcher instance, which records “touched” data properties as dependencies during component rendering. Watcher is then notified when the setter for the dependency fires, causing its associated component to be re-rendered.

Deep into Vue responsive principle

Common object 🆚 Common object passed to Vue

const myData = { // myData is an ordinary object
  n: 0
}
console.log(myData)  // (output is as follows)
Copy the code

const myData = { // myData is an ordinary object
  n: 0
}
new Vue({
  data: myData  // Pass myData as the data option to the Vue instance
})
console.log(myData) // See what myData looks like now.
Copy the code

What does Vue do with Data

One of the most unique features of Vue is its non-invasive responsive system. Data models are just plain old JavaScript objects. And when you modify them, the view is updated.

When we pass the data option to the Vue instance, the Vue “listens and proxies” through all the properties in the data.

  • Listening: Low-level details of data responsiveness
  • Proxy: Does not expose any interface to prevent users from tampering with data

To transform this data into non-invasive responsive data

Front knowledge

Object.defineProperty

The MDN: object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object.

use

Object.defineProperty(obj, prop, descriptor)
// obj stands for object that defines attributes; Prop represents the name of the defined property; Descriptor stands for something defined.
Copy the code

Requirement ⭕️ add attribute n with object.defineProperty with a value of 0

let data1 = {}
Object.defineProperty(data1, "n", {  // add attribute "n" to object data1 with a value of 0
  value: 0
}) 
console.log(data1)    // { n: 0 }
console.log(data1.n)  / / 0
Copy the code

getter / setter

MDN: getter/setter description, example

  • Get: The getter function for the property. This function is called when the property is accessed. No arguments are passed
  • Set: the setter function for the property. This function is called when the property value is modified. Accepts a parameter (that is, the new value given)

Need ⭕️ to add attribute N with object.defineProperty and listen for n to read.

  • Use Object.defineProperty to convert all of these properties into getters/setters.
  • Set the getter/setter so that when we read n we automatically get the getter and when we modify n we automatically get the setter
let data2 = {}
data2._n = 0  // Add a media attribute _n to store the original value 0 of n
Object.defineProperty(data2, "n", {
  get() {
    // return this.n why does the getter not return this.n?
    return this._n
  },
  set(value) {
    this._n = value
  }
})
console.log(data2.n) / / 0
Copy the code

Question: Why doesn’t the getter just return this.n instead of declaring _n to store n and using _n as a relay?

The reason:

  • All operations that read n (data2.n, this.n) automatically fetch the return value of the getter method as the value of n
  • If n is read and the getter returns this.n, this.n is still reading n, and then it automatically gets the getter, so it becomes an “infinite loop”, reading n continuously and never getting the final value
  • So declare a media property _n to store n values. When n is read, the getter returns _n, and when n is written, the setter changes _n

Confusing: why not just define the attribute n: 0 when you declare the Object. Doesn’t using Object.defineProperty complicate the process?

let data1 = { n: 0 } 
console.log(data1.n)  / / 0
data1.n = 100
console.log(data1.n)  / / 100
Copy the code

Isn’t it nice to create objects in literal form that are easy to read and write? A: No smell (read on to see why)

Why use this API

When requirements become complex ↓

  • Requirement ⭕️ Add attribute n, default value 0. Also, the assignment of n can never be less than 0
  • For example: assignmentdata2.n = -1Is invalid, assignmentdata2.n = 1effective
let data2 = {}
data2._n = 0
Object.defineProperty(data2, "n", {
  get() {
    // others ...
    return this._n 
  },
  set(value) {
    if (value < 0) return  // 👈 If value is less than 0, the value is returned without assignment
    this._n = value
    // others ...}})Copy the code

So, use Object.defineProperty to convert all of the Object’s attributes to getters/setters

  • This allows additional operations to be added at the same time as the read/write trigger.
  • Vue uses this feature of Object.defineProperty to implement the effect of data monitoring

Listening logic 📌

The responsivity principle of Vue is implemented through Object.defineProperty

The sample

/ / the demo. Vue components
const myData = {
  name: 'jack'.sex: 'male'.age: 20
}
new Vue({
  data: myData,  // When we pass myData as data, Vue will... 🔗
  template: '
      
Name: {{name}} Age: {{age}}
}).$mount("#app") Copy the code

Simulate Vue internal operations

🔗 Vue iterates over the data and transforms each item in the data into a getter/setter via Object.defineProperty

// When Vue receives myData, it iterates through the Object and modifies it with Object.defineProperty
for(let key in myData) {
  let val = data[key] // val stores the original value of the current traversal attribute
  Object.defineProperty(data, key, {
    get() {
      // others... Notify Watcher to add a dependency 📌
      return val // Trigger the get function to return the value of the attribute
    },
    set(newVal) {
      val = newVal // Trigger the set function to change the value of the attribute
      // others... Notify Watcher to render the associated component 📌}})}Copy the code

Each component instance corresponds to a Watcher instance

  • During component rendering, the getter fires data that Watcher records as “render view dependencies.”
  • When the setter for the dependency fires (indicating that the data has been modified), Watcher is notified and its associated components are rerendered

The sample analysis

/ / the demo. Vue components
const myData = {
  name: 'jack'.sex: 'male'.age: 20
}
const vm = new Vue({
  data: myData,
  template: '
      
Name: {{name}} Age: {{age}}
// 👆 view rendering, which triggers getters for name and age, notifying Watcher that the record properties are dependencies }).$mount("#app") myData.name = 'rose' // 👆 fires the setter for name, and the setter for the dependency is fired, telling Watcher to re-render the component Copy the code

The above

A simple data response formula is implemented in this way

Extension: the logic of agency

The requirement ⭕️ stores an attribute n to the object as long as the assignment of n is never less than 0

  • May I ask, how can we ensure that “no matter how users modify the value of N, it can meet the above requirements”?

The experiment of a

(Online example)

let myData = {}
myData._n = 0 // Always store n values with _n (_n is equivalent to a relay attribute)
Object.defineProperty(myData, "n", {
  get() {
    return this._n // It must be relayed. If you return this.n directly, you will fall into an infinite loop
  },
  set(value) {
    if (value < 0) return
    this._n = value
  }
})
myData.n = -1
console.log(myData.n) // 0 is invalid
myData.n = 1
console.log(myData.n) // 1 Is successfully modified
Copy the code
gaming

Online sample

Mydata. _n = mydata. _n = mydata. _n = mydata. _n = mydata. _n = mydata. _n
// Although n cannot be set to a value less than 0, _n can be. It is just a normal attribute on myData. There is no set listening, and it can be assigned arbitrarily

myData._n = -1
console.log(myData.n) // -1 Successfully tampered with
Copy the code

Conclusion: Experiment 1 can bypass monitoring and easily tamper with data n by modifying relay attributes

Experiment 2

According to the “loophole” of experiment 1, the following solutions are proposed:

  • Do not expose any non-listening attribute interfaces on myData objects (preferably without object names)
  • In other words, the requirement is ⭕️ for any access, any interface, to be listened on
  • If the data itself is the return value of a function, then the function will be executed every time the data is called. We only need to realize the monitoring of the data inside the function, so that users can prevent stealing and tampering with the data
  • The concrete realization refers to the idea of “agent”

Write a proxy function, parameters receive an anonymous object, in the anonymous object to store real data, in the proxy function to achieve the data monitoring proxy

  • Anonymous objects are used because objects that have not been named in advance cannot be accessed by outsiders
  • Every time the data on myData is called, the proxy method will be executed to transfer the real data to OBJ for listening, and then return OBJ to ensure that when the property of myData is called, the monitored property on OBJ must be called
  • You can ensure that myData must not expose any unmonitored attributes (_n), all attributes are listened through proxy processing

Online sample

let myData = proxy({ data: { n: 0}})function proxy({ data }) {  Data {n: 0}, which stores the real data
  const obj = {} 
  // Transfer all data from data to obj for listening, then return obj, so that all data on obj is listened
  Object.defineProperty(obj, "n", { 
    get() {
      return data.n
    },
    set(value) {
      if (value < 0) return;
      data.n = value
    }
  })
  return obj // obj is a proxy object for data
}

myData.n = -1
console.log(myData.n) // 0 is invalid
myData.n = 1
console.log(myData.n) // 1 Is successfully modified
Copy the code
gaming

If the user writes his own data object hackData

You can tamper with the data by putting the hackData into the agent and then modifying the hackData

Online sample

let myData = proxy({ data: { n: 0}})// Raw data
let hackData = { n: 0 } // Declare new data, used to tamper with original data
myData = proxy({ data: hackData })

function proxy({ data }) {
  const obj = {} 
  Object.defineProperty(obj, "n", { 
    get() {
      return data.n
    },
    set(value) {
      if (value < 0) return;
      data.n = value
    }
  })
  return obj
}

console.log(myData.n) / / 0
hackData.n = -1
console.log(myData.n) // -1 Successfully tampered with
Copy the code

Conclusion: A user can force a hackData into the proxy so that only hackData can tamper with the raw data n

The experiment of three

According to experiment two, “loophole”

  • The user forced a hackData into the proxy to tamper with our original data n
  • Requirements ⭕️ If users tamper with data (passing in their own written data), intercept this behavior (upgrade Proxy 2.0)

Online sample

let myData = proxy2({ data: { n: 0}})// Raw data
let hackData = { n: -1 }  // Declare new data, used to tamper with original data
myData = proxy2({ data: hackData })

function proxy2({ data }) {  // data receives hackData: {n: 0}
  // It would have traversed all the keys on data, but this is simplified, assuming there is only one data n on data
  let value = data.n  // Declare a variable to store the original n value.
  
  // delete data.n  
  // You can omit the delete operation, because the following virtual attribute n has the same name as the original attribute in data, and the original attribute will be overwritten (deleted) automatically
  
  Object.defineProperty(data, "n", { // This will completely monitor n
    get() {
      return value
    },
    set(newValue) {
      if (newValue < 0) return
      value = newValue
    }
  })
  // Add new code to listen on n completely
  // Install a listener. If the user wants to bypass the listener, they will immediately know and intercept you

  // Here is the proxy logic
  const obj = {}
  Object.defineProperty(obj, "n", {
    get() {
      return data.n
    },
    set(value) {
      if (value < 0) return 
      data.n = value
    }
  })
  return obj // obj is the proxy
}

// If you modify myData directly, follow the normal proxy logic.
// If the user wants to bypass the agent and tamper with raw data, the listener logic will be used
// As long as it passes through proxy2, it must be under listening because it will delete the original data n

// Tamper with the original data (create virtual attributes to replace the original attributes, by listening on virtual attributes to achieve data listening)
hackData.n = -1  
console.log(myData.n) // 0 is invalid
hackData.n = 1
console.log(myData.n) // 1 Is successfully modified

// Modify directly (via proxy)
myData.n = -1
console.log(myData.n) // 1 is invalid
myData.n = 0
console.log(myData.n) // 0 Succeeded
Copy the code

conclusion

// The following code looks familiar.
let myData = proxy2({ data: hackData })
let vm = new Vue({ data: hackData })

// myData is the vm
// Proxy2 is equivalent to new Vue, but without new
Copy the code

The previous experiment launched proxy code, is Vue internal source code

Be sure to note 💡
  • The research method of the previous experiment is more important than the knowledge itself
  • These methods allow you to understand the truth without reading the source code (underlying principles)
What does New Vue do with Data

Having cleared up the agent logic, now we can talk about what new Vue does with Data

import Vue from "vue/dist/vue.js"

const hackData = {
  n: 0
}
console.log(hackData) // quintessence 👈👈👈 finds the difference through log, thus conducting inference verification

const vm = new Vue({
  data: hackData,
  template: `<div>{{n}}</div>`,
}).$mount("#app")

setTimeout(() = > {
  hackData.n += 10;
  console.log(hackData) / / essence 👈 👈 👈
}, 0)
Copy the code

Once hackData is passed to new Vue, hackData will be tampered with immediately

  • – hackData data n will disappear and get N, set N will be replaced
  • The key is to find clues in two logs (this method is important)

Monitor existing problems (Vue bugs) 📌

We know that data should be defined in advance in data before it can be used. What if you dynamically add new data outside of data, or reference data not defined in data?

Scenario 1: What happens when you reference undefined data

What if n is not defined in the Vue instance’s data, but is used in the view

Code sample

new Vue({
  data: {},
  template: ` 
      
{{n}}
`
}).$mount("#app") Copy the code

Vue warns when using data not defined in data in a view.

  • If data is not passed into the data of the instance, it cannot be implemented to listen for data changes. If data is not listened, it cannot be implemented to refresh the view, violating the principle of data responsiveness

The solution

N must be pre-declared in data

Since Vue does not allow you to dynamically add root-level responsive properties, you must declare all root-level responsive properties, even if they are null (document), before initializing the instance.

Online code

new Vue({
  data: {
    n: 1 // 👈👈👈 null, undefined.. No error will be reported
  },
  template: ` 
      
{{n}}
`
, }).$mount("#app") Copy the code

Scenario 2: Dynamically adding object attributes (adding keys to data)

Requirement: Click the button to display 1 in the view

Code sample

new Vue({
  data: {
    obj: {
      a: 0 // obj. A will be monitored by Vue}},template: ` < div > obj. B: {{obj. B}} < button @ click = "set" > value < / button > < / div > `.methods: {
    set() {             
      this.obj.b = 1  // Will the page display 1?
    }
  }
}).$mount("#app") 
Copy the code

If I click the assign button, will 1 appear in the view?

  • Answer: 1 is not displayed.
  • Because Vue cannot listen to data that is not defined in dataobj.b
  • But why does referencing undefined obj.b give no warning?

Scenario 1’s reference to undefined n in data is an error, so why is scenario 2’s reference to undefined obj.b not an error?

  • The data response of Vue is the root response of the data. Vue checks only the first level properties, not those defined inside the first level properties
  • In other words, forthis.obj.bVue will only check if there is a definition in datathis.objIf obJ is not defined, an error will be reported, but Vue will no longer examine the internal properties of OBJ in depth
  • Scenario 2 view refers to an undeclaredobj.bBut Vue will checkobj It’s already declared in data, so the reference here doesn’t give an error, but it doesn’t take effect becausebIt is not declared in data.
  • In summary, this use of scenario 2 simply does not report an error, but it does not render the view

The solution

Set or this.$set to add new attributes to an existing object (document)

Online code

new Vue({
  data: {
    obj: {
      a: 0}},template: ` < div > obj. B: {{obj. B}} < button @ click = "set" > value < / button > < / div > `.methods: {
    set() {
      Vue.set(this.obj, 'b'.1)       / / 👈 👈 👈
      $set(this.obj, 'b', 1) // 👈👈👈
      // There is no difference between the two methods.
      console.log(Vue.set === this.$set) // true ($is added to prevent data from having the same property set)
    }
  }
}).$mount("#app")
Copy the code

Vue. Set and this.$set perform what operations

Vue.set(obj, key, value)  // Add a key attribute to obj
Copy the code
  • The new key
  • Automatically create proxy and listener for key (if not already created)
  • Trigger UI updates (but not immediately — asynchronously, this is worth a separate blog post)
    • The UI update is performed by Vue, and the set only triggers Vue to perform the update

Example demonstrates

new Vue({
  data: {
    obj: {
      a: 0  / / 👈}},template: ` < div > {{obj. B}} 👈 < button @ click = "setB" > set b < / button > 👈 < button @ click = "addB" > add b < / button > 👈 < / div > `.methods: {
    setB() {
       Vue.set(this.obj, 'b'.1) $set(this.obj, 'b', 1) $set(this.obj, 'b', 1)
    },
    addB(){
      this.obj.b += 1  / / 👈 👈
    }
  }
}).$mount("#app");
Copy the code

Scenario 3: How to deal with arrays in data?

Is there a case where “there is no way to declare the key in data in advance”

  • Yes, if the data is an array type, you can’t declare all the elements in advance
  • Arrays in Vue can be thought of as objects (see below)

Try: Adding array elements dynamically

The sample

new Vue({
  data: {
    array: ["a"."b"."c"]},template: ` 
      
{{array}}
`
.methods: { setD() { this.array[3] = "d" // Will the page display 'd'? A: no } } }).$mount("#app"); Copy the code

In the code above, click the button and add an element to the array. Nothing happens. Why?

  • Analysis: Arrayarray: ["a", "b", "c"]Understood as object form 👇
{
  0: "a".1: "b".2: "c".length: 3
}
Copy the code

The data array in data is equivalent to declaring 0, 1 and 2 attributes in data

  • Add a key to an array (object), add a key to an array (object), add a key to an array (object), add a key to an array (object), add a key to an array (object)
  • sothis.array[3] = "d"Is invalid

In combination with “scenario 1 and scenario 2”, the solution to “referencing undeclared data” or “dynamically adding object attributes” is given:

  • Scenario 1: Root-level attributes directly referenced in a view must be declared in data in advance
  • Scheme 2: dynamically add object attributes, need to use set method

If you use plan 1: define the data in an array ahead of time

data:{
  array: ['a'.'b'.'c'.undefined.undefined. ] },methods: {
  setD() {
    this.array[3] = "d"  
    // It is possible, but it is impossible to predict how many keys there are and how many undefined keys there should be
    // Write all the keys to array ahead of time}}Copy the code
  • The length of the array can always increase (the subscript is key). It is impossible to predict how many keys there will be (if the array is a list of all users). It is impossible to declare all the keys in advance

If option 2 cannot be declared in advance, use vue. set or this.$set

this.$set(this.array, 3.'d')
// set(Array, Index, value)
Copy the code
  • Adding an array key via set is possible. But executing set every time you add a new element can be tedious
  • Is there an easier way?

Yu Yu Creek’s approach

Test: Since it is an array, can I add elements using the array method push?

Result: Yes, push takes effect

this.array.push('d')
console.log(this.array) // Take a look at push in vue
Copy the code

Confusion: Why does adding elements directly fail, but pushing does?

Reason: The push method in Vue is no longer the push method in the array prototype.

As shown, array looks like a normal array, but when we pass array to Vue, Vue tampers with the array by inserting a layer of prototypes in the middle with seven methods. This 7 method has the same name as the method on the array prototype (the code has been modified) and overrides the prototype method when called

The seven new apis do two things

  • First, the new API calls the “old API” method

  • Then, add “Listener & Proxy” to the array

conclusion

  • Vue tampered with the array API (explained in the “mutating methods” section of the Vue documentation)
  • Each of these 7 apis is tampered with by Vue, which updates the UI when called

How exactly did you tamper with it? 📌

Yu’s idea: Insert a layer of prototypes with seven methods

  • Here is the “mock” tamper code (how to insert a layer of prototypes)
ES6 writing
The demo test

Through the idea of inheritance, implementation adds a layer of prototype

class VueArray extends Array { 
  VueArray currently inherits from Array, meaning that VueArray precedes the Array prototype
  push(. args) { // declare a push on VueArray as a common property that will be placed on VueArray's _proto_
    console.log("args", args)
    console.log("arguments".arguments)
    console.log("... args". args)super.push(... args)// indicates that VueArray's push is performed by calling the native push method on Array
    console.log('You pushed')}}const a = new VueArray(1.2.3.4)
console.log(a)
a.push(5) // Add element 5 to the array and execute log
console.log("a", a)
Copy the code

So what I’m going to do is I’m going to write this

  • When we execute VueArray’s push method, the effect is the same as the native push method, but with an additional console.log
  • There is only one push method on the first prototype, and the second prototype is the Array prototype
Simulate full logic 📌
class VueArray extends Array{ 
  push(. args){
    const oldLength = this.length  // This is the current array (new array instance) record the length of the old array
    super.push(... args)// After push, this.length has been updated
    console.log('You pushed') 
    
    for(let i = oldLength; i < this.length; i++){ 
      Vue.set(this, i, this[i])  
      // Tell Vue about each new key, and Vue knows about the data changes, the need to add agents & listeners, and update the view}}}const a = new VueArray(1.2.3.4)
console.log(a)
a.push(5)
Copy the code

Note: This code is for logical understanding only. In fact, this implementation is inefficient and does not represent a true implementation of Vue.

Frank didn’t actually read the source code (but that’s the general idea of Rainy Creek)

ES5 – Prototype

ES5 code is harder to understand than ES6 (ES6 lowers the threshold for the front-end)

  • Implementation: Insert a new prototype layer, the new prototype is the instance, the lower layer is the array prototype
  • The new prototype has push methods: perform some manipulation that the developer has tampered with, and then call the push implementation of the array prototype to add elements
const vueArrayPrototype = { 
  push: function(){ 
    console.1og('You pushed') // All the other code added here is tampering with push
    return Array.prototype.push.apply(this.arguments) / / passthrough
    // 👆 user passed me something, I passed everything to "push method on array prototype"
  }
}
vueArrayPrototype.__proto__ = Array.prototype // Point my prototype to the array's prototype to form the prototype chain
// This sentence is not a standard attribute, only learn to use

const array = Object.create(vueArrayPrototype) // Create an array using the new prototype I wrote
array.push(1) 
console.log(array)  // You can see the chain effect of three prototypes: one for the object itself, two for the new prototype, and three for the array prototype
Copy the code

conclusion

The new key in the object

  • Vue has no way to listen and proxy in advance (resulting in key changes that do not affect the UI at all)
  • You use set to add keys, create listeners and proxies, and update the UI
  • It’s best to write all the attributes out ahead of time, not add new keys
  • Note: Arrays cannot write the key property in advance

The new key in the array

  • Arrays can be used to add keys and update the UI
    • This.$set does not automatically add listeners and proxies when applied to arrays.
      • You can test it and use it after you set itthis.array[n] += 1Does it trigger UI updates (the answer is no)
    • When using the array variation API provided by Vue, listeners and proxies are automatically added
  • However, Rain Creek has tampered with seven apis to make it easier for developers to manipulate arraysAdd or delete
    • Add, because Vue cannot listen, you need to modify the code so that Vue can listen
    • Delete, although Vue has been listening to, but the native API directly delete, Vue is not aware of, there will be redundant listeners, waste memory, so delete API also modify, implement at the same time to delete the listener
    • Change and check: these two operations must take place in the Vue listening environment, there is no need to do extra processing for Vue, just use the native API
  • These 7 apis automatically handle listeners and proxies and (asynchronously) update the UI
    • Push, POP, Shift, unshift, splice, sort, reverse
    • The first 5 are necessary, the last 2 May be provided for convenience, not very useful, of course, if there is a need to use the API provided by Vue, more efficient