Recently, I have been reading some basic knowledge. So I want to make a series of attempts to talk about these complicated and important knowledge points. Learning is like a mountain, only their own to climb, to see different scenery, experience more profound. Today we are going to talk about the more important reactive principles and dependency collection in Vue.

Response principle

Object.defineproperty () and Proxy objects can both be used to hijack data. What is data hijacking? When we access or modify a property of an object, we intercept it with a piece of code, and then do something extra to return the result. Two-way data binding in VUE is a typical application.

Vue2. X uses object.defindProperty () to listen on objects.

The Vue3. X version was followed by a Proxy implementation.

In MDN it is defined as follows:

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

Object.defineProperty(obj, prop, descriptor)

  • Obj: Object for which attributes are to be defined
  • Prop: The name of the property to define or modify
  • Descriptor: a property descriptor to be defined or modified (64x). Writable: writable; I’d like to enumerate. Get \set: sets or retrieves the value of an object property.
const data = {}
const name = 'zhangsan'
Object.defineProperty(data, 'name', {
    writable: true,
    configurable: true,
    get: function () {
        console.log('get')
        return name
    },
    set: function (newVal) {
        console.log('set')
        name = newVal
    }
})
Copy the code

When passing an ordinary JavaScript object to 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. When vue initData is used, the data above _data is proxy to vm. All data can be observed through observer class, and getter\setter operation is performed on each property defined by data. This is the basis for Vue’s responsive implementation.

Responsive Principle (Observer)

The Observer class converts the key value of each target object (that is, data) into getter/setter form for dependency collection and scheduling updates. So how is this class implemented in Vue?

  • 1. The Observer instance is bound to the OB property of data to prevent repeated binding;
  • 2. If data is an array, first implement the corresponding variation method (Vue has rewritten 7 native methods of the array) and then observe each member of the array to make it a responsive data;
  • 3, otherwise execute walk(), iterate over all data, getter/setter binding. The core method here is defineReative(obj, keys[I], obj[keys[I]])
Class constructor(value) {this.value = valueif(! value || (typeof value ! = ='object')) {
      return
    } else {
      this.walk(value)
    }
  }
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}
Copy the code
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      return val
    },
    set: functionReactiveSetter (newVal) {// Note: value is always in the closure, and after setting it, the latest value will be returned when you get itif (newVal === val) return
      updateView()
    }
  })
}
function updateView() {
  console.log('View updated')
}

const data = {
  name: 'zhangsan',
  age: 20
}
new Observer(data)

data.name = 'lisi'// Print 'View updated'Copy the code

This is a simple Observer class, and this is the rationale behind vUE responsiveness. But we all know that Object.defineProperty has some disadvantages:

1, for complex objects need deep monitoring, recursive to the end, one-time calculation is large

Vue. Set Vue. Delete

3, can not listen to the array, need special processing, that is, above said mutation method

This is an improvement of Vue3, and we will focus on how vue3 Proxy can be responsive.

How does VUE listen in depth

In the figure above, we can see that the first level directories of data, name and age, will start the view update when the value changes. However, in our actual development process, data may be a relatively complex object, nested several layers:

const data = {
  name: 'zhangsan',
  age: 20,
  info: {
      address: 'Beijing'
  }
}
data.info.address = 'Shanghai'// No execution.Copy the code

The reason for this is that the val received by defineReactive in the code is an object. In order to avoid this complex object, Vue uses the recursive idea of executing observer function once in defineReactive, recursively iterating the object once to obtain the key/value value. New to the Observer (val). Also, name may be set as an object when setting the value, so depth monitoring is also needed when data value is updated

functionDefineReactive (obj, key, val) {new Observer(val) {Object defineProperty(obj, key, {enumerable:true,
    configurable: true,
    get: function reactiveGetter() {
      return val
    },
    set: functionReactiveSetter (newVal) {// Note: value is always in the closure, and after setting it, the latest value will be returned when you get itif (newVal === val) returnNew Observer(val) updateView()}}Copy the code

Extension 2, vUE array listening

Object.defineproperty does not work on arrays, so how to listen for array changes in vUE, in fact, vUE is listening for the array change method wrapped. We’ll demonstrate this in simple code:

Const oldArrayProperty = array. prototype // Create a new object, Const arrProto = Object.create(oldArrayProperty); ['push'.'pop'.'shift'.'unshift'.'splice'].forEach(methodName => {
  arrProto[methodName] = function() {// Define array method updateView() oldArrayProperty[methodName]. Arguments) // Actually execute the array method}}) // handle the array in the Observer functionif (Array.isArray(value)) {
    value.__proto__ = arrProto
  }
Copy the code

As can be seen from the code, there is a layer of array interception in the Observer function, and the array __proto__ points to an arrProto. ArrProto is an object that points to the array prototype, so arrProto has the method on the array prototype. We then recustomize the array’s 7 methods on this object and wrap them around them without affecting the array prototype. This is variation, and observe each member of the array to make it responsive.

Dependency collection (Watcher, Dep)

We now have such a Vue object

new Vue({
    template: 
        `<div>
            <span>text1:</span> {{text1}}
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2'}})Copy the code

We can see from the above code that Text2 in data is not actually used by the template. In order to improve the efficiency of code execution, we do not need to respond to it. Therefore, the simple understanding of dependency collection is to collect data only used in the actual page. This is the Watcher, Dep class.

When the getter is triggered by the Observer’s data, the Dep collects the dependency and marks it, in this case as dep.target

Watcher is an observer object. After the dependency collection, the Watcher object is stored in the SUBs of the Dep. The Dep notifies the Watcher instance when the data changes, and the Watcher instance calls cb to update the view.

Watcher can accept subscriptions from multiple subscribers, and when data changes, Watcher is notified via Dep of updates.

We can implement this process with some simple code.

class Observer {
  constructor(value) {
    this.value = value
    if(! value || (typeof value ! = ='object')) {
      return
    } else{ this.walk(value) } } walk(obj) { Object.keys(obj).forEach(key => { defineReactive(obj, key, Obj [key])})} // Subscriber Dep {constructor() {this.subs = []} /* Add an observer */ addSub (sub) {this.subs.push(sub)} /* Rely on collection to add an observer */ when there is a dep.targetdepend() {
    if(dep.target) {dep.target.adddep (this)}} // Notify all Watcher objects to update the viewnotify () {
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}
class Watcher {
  constructor() {/* Assign a new Watcher object to dep.target. */ dep.target = this; }update () {
    console.log('View updated'AddDep (dep) {dep.addSub(this)}}function defineReactive (obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {dep.depend() /* To collect dependencies */return val
    },
    set: function reactiveSetter (newVal) {
      if (newVal === val) returndep.notify() } }) } class Vue { constructor (options) { this._data = options.data new Observer(this._data) // // Create an observer instance console.log()'render~', this._data.test)
  }
}
let o = new Vue({
  data: {
    test: 'hello vue.'
  }
})
o._data.test = 'hello mvvm! '

Dep.target = null
Copy the code

conclusion

  • In Vue, directives or data bindings during template compilation will instantiate a Watcher instance, which will trigger get() to point itself to dep.target.
  • Getters in the Observer trigger dep.depend() for dependency collection.
  • When the value of a data object is changed by an Observer, the watcher in subs that observes it executes the update() method, which actually calls the watcher callback cb to update the view.

Vue3-Proxy is responsive

Proxy can be understood as setting up an interception layer in front of the target object. The external objects must pass this interception layer before starting the object. Therefore, Proxy provides a mechanism to filter and rewrite the external access.

function reactive(value = {}) {
  if(! value || (typeof value ! = ='object')) {
    return} // Proxy configuration const proxyConf = {get(target, key,receiver) {// Only non-prototypical properties are handledlet ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('get', key)} const result = reflect. get(target, key, receiver) When to recursereturn reactive(result)
    },
    set(target, key, val, receiver) {// Repeated data is not handled const oldVal = target[key]if (val === oldVal) return true
      
      const ownKey = Reflect.ownKeys(target) 
      if (ownKeys.include(key)) {
          console.log('Existing key', key)
      } else {
          console.log('New key', key)
      }
      
      const result = Reflect.set(target, key, val, receiver)
      console.log('set', key, val)
      return result
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      console.log('delete property', key)
      returnConst observed = new Proxy(value, proxyConf)return observed
}
const data = {
  name: 'zhangsan',
  age: 20,
  info: {
    address: 'Beijing'
  },
  num: [1, 2, 3]
}
const proxyData = reactive(data)
proxyData.name ='lisi'  // set name lisi
Copy the code

Proxy deep listening improves performance. For complex objects in the proxy, only geter() is used to listen on the current layer, such as in info

info: {
    address: 'Beijing',
    a: {
        b: {
            c: {
                d: 2
            }
        }
    }
}
Copy the code

Modifying proxydata.info. a does not recurse b, C and D, avoiding the recursive calculation of Object.defineProperty at one time. Because proxy natively listens on arrays, this is an improvement over object.defineProperty’s shortcomings. In addition, it can be seen from the code that the proxy can also listen when adding/deleting, which is the advantage of proxy.

Expand 1 Reflect

The reflect method corresponds to the proxy method. As long as it is a proxy method, the corresponding method can be found in the Reflect object. This makes it easy for the proxy object to call the corresponding Reflect method to perform the default behavior as a basis for modifying the behavior.

Reflect normalizes Object by putting some of the Object’s methods (such as Object.defineProperty) that are clearly internal to the language.

Reflect.get(target, name, receiver): Retrieves and returns the name attribute on the target object, without which undefined is returned

Reflect.set(target, name, value, receiver): Sets the name property of the target object to value

Reflect.has(object, name): Checks whether the object has a name attribute

Reflect.ownkeys (target): Returns all properties of the object

Extension 2: Use proxy to implement the observer pattern

// Observer mode refers to the mode in which the function automatically observes the data object, and if the data changes, Const queuedObservers = new Set() const observe => queuedobServers.add (fn) const Observable = obj => new Proxy(obj, {set})

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver)
  queuedObservers.forEach(observe => observe())
  returnResult} const person = Observable ({// observable name:'Joe',
  age: 20
})
function print() {// Observer console.log('${person.name}.${person.age}`)
}
observe(print)
person.name = 'bill'

Copy the code