A brief discussion on the responsive principle of Vue2.0

In Vue, responsiveness refers to the establishment of an automatic association between data and views. When the data changes, the view will automatically update to reflect the changes in the data, without the need for developers to manually operate the DOM. The element updates the view.

Responsiveness is the basis for implementing data-driven views. In Vue, data can be understood as statestate, and the view is what the user intuitively sees< strong>PageUI. The page will dynamically change as the status changes, so the following formula can be obtained:

UI = render(state)

In the above formula: state state is the input, and the page UI is the output. Once the state input changes, the page output will also change accordingly. This feature is called Data-driven view.

The formula can be further split into three parts: state, render(), and UI. Among them, state and UI are defined by users or implemented by developers, and what remains unchanged is the render() function. So Vue plays the role of render(). When Vue detects the change of state, after After a series of processing, changes will eventually be reflected on the UI interface in a responsive manner.

Implementing data responsiveness requires solving the following three problems:

  1. How to monitor changes in state state?
  2. How to determine which view to update after the state state changes?
  3. When is the time to update the view?

1. Monitoring state changes

In Vue2, data monitoring is implemented with the help of the Object.defineProperty() function provided by javaScript.

1. Object.defineProperty()

Object.defineProperty()Static method is used to define a new property on an object or modify an existing property of the object.

Object.defineProperty(obj, prop, descriptor)

Among them, descriptor represents the attribute descriptor object to be defined. There are two types: data descriptor and accessor descriptor< /strong>.

Note: A descriptor can be of only one of these two types, and the two cannot be mixed.

  • A data descriptor is a property with a writable or non-writable value. The object supports four key values: value, enumerable, writable, configurable

    const person = {<!-- -->}
    Object.defineProperty(person, 'age', {<!-- -->
      value: 18, //Define attribute value, default is undefined
      enumerable: true, // Attributes can be enumerated, default false
      writable: true, // Attribute value can be modified, default false
      configurable: true, // Attributes can be deleted, default false
    })
    
    console.log(person.age) // > 18
    person.age = 19
    console.log(person.age) // > 19
    
  • Accessor descriptors are properties described by getter/setter function pairs and support four key values: enumerable, configurable, get , set

    const person = {<!-- -->}
    let age = 18
    Object.defineProperty(person, 'age', {<!-- -->
      enumerable: true,
      configurable: true,
      get() {<!-- -->
        console.log(`Someone has read person's age, current age ${<!-- -->age}`)
        return age
      },
      set(value) {<!-- -->
        console.log(`Someone modified the person's age, the new age is ${<!-- -->value}`)
        age = value
      },
    })
    
    person.age
    person.age = 19
    age
    

Output result:

1

2. Object change monitoring in Vue2.0

Vue2.0 uses the accessor descriptor in the Object.defineProperty method to hijack data read and write operations. Capture data reading events in getter, capture data modification events in setter, and then monitor data changes.

For data of type Object, Vue sets each property in the data to the form of getter/setter through recursive traversal, so that the object Every attribute of becomes observable

The following is a simple implementation of Object type data monitoring:

  • Simulate a function that updates the view

    /**
     * @description: update view function
     */
    function updateView() {<!-- -->
      console.log('Received notification, I went to update the view')
    }
    
  • Define the Observer class

    In vue, all responsive data are instance objects of the Observer class.

    /**
     * @description: Define the Observer class to convert all properties of an object into observable objects
     * @return {*}
     */
    class Observer {<!-- -->
      constructor(value) {<!-- -->
        this.value = value
        //Add a new __ob__ attribute to value, whose value is the Observer instance of value
        // This is equivalent to marking the value to indicate that it has been converted into a responsive format to avoid repeated operations.
        Object.defineProperty(value, '__ob__', {<!-- -->
          value: this,
          enumerable: false,
          writable: true,
          configurable: true,
        })
        this.walk(value)
      }
      walk(value) {<!-- -->
        const keys = Object.keys(value)
        for (let i = 0; i < keys.length; i + + ) {<!-- -->
          defineReactive(value, keys[i]) // Traverse all properties and convert all property values into getter/setter form
        }
      }
    }
    
  • Define reactive functions

    //Source code location:/src/core/observer/index.ts
    /**
     * @description: Recursively define responsiveness for the object's properties and set getters/setters so that the read events of the object's properties can be monitored.
     */
    function defineReactive(obj, key, value) {<!-- -->
      if (arguments.length === 2) {<!-- -->
        value = obj[key]
      }
      if ( // If the value is object or array, call the observer function recursively to implement the responsiveness of value
        Object.prototype.toString.call(value) === '[object Object]' ||
        Array.isArray(value)
      ) {<!-- -->
        let childObj = observer(value) // Recursively call the listening function to monitor the child properties
      }
      Object.defineProperty(obj, key, {<!-- -->
        configurable: true,
        enumerable: true,
        get() {<!-- -->
          console.log(`Someone read the ${<!-- -->key} attribute, and the attribute value is ${<!-- -->value}`) // Capture the data reading event
          return value
        },
        set(newValue) {<!-- -->
          if (newValue !== value) {<!-- -->
            console.log(
              `Someone modified the ${<!-- -->key} attribute, and the modified value is ${<!-- -->newValue}. I want to send a notification to update the view! `
            ) // Capture data modification events
            childObj = observer(newValue) //The updated value must also be monitored
            value = newValue
            updateView() // Notify update view
          }
        },
      })
    }
    
  • Define listening function

    /**
     * @description: listening function
     */
    function observer(value) {<!-- -->
      if ( // Only data of object or array type needs to be monitored
        Object.prototype.toString.call(value) === '[object Object]' ||
        Array.isArray(value)
      ) {<!-- -->
        if (typeof value.__ob__ !== 'undefined') {<!-- --> // value is already responsive data and directly returns the observer instance
          return value.__ob__
        }
        return new Observer(value) // Construct an observer instance with value as a parameter and return
      }
    }
    

Verify implementation:

const data = {<!-- -->
  name: 'zhangsan',
  age: 19,
  address: {<!-- -->
    city: 'beijing',
    country: 'China',
  },
}
const ob = observer(data)
console.log(ob)

Output result:

Then perform some operations on the data in data:

const data = {<!-- -->
  name: 'zhangsan',
  age: 19,
  address: {<!-- -->
    city: 'beijing',
    country: 'China',
  },
}

data.name
data.name = 'lisi'
data.address.city
data.address.city = 'shanghai'

Output result:

As you can see, we captured events for reading object properties and modifying object property values. For nested objects, it is also possible to monitor the reading and modification of the property values of the nested objects.

It is worth noting that the modification of the internal property value of the nested object cannot be sensed by the outer object. This is why in the monitoring function watch, if you want to implement deep monitoring, you must use deep.

Process Summary:

For a piece of data, first call the observer function to implement monitoring. If the incoming data is an object that has not been monitored, then new a Observe instance object and return, otherwise, return the observer instance object corresponding to the data.

In the constructor of the Observer class, add a __ob__ attribute to the passed in object, the value of which is the observer of the object. The code> instance object is used to mark the object to indicate that the object has been converted into a responsive object to avoid repeated operations.

Then call the walk function to convert each attribute into the form of getter/setter to monitor data changes.

Finally, in the defineReactive function, when the incoming attribute value is still an object, the observer function is used recursively to monitor the nesting of the object. Object, so that all attributes (including deep attributes) in data can be converted into the form of getter/setter to deeply monitor changes in object data.

3. Array change monitoring in Vue2.0

Array itself is also an object, and also supports using defineProperty to set getter/setter for responsive monitoring of elements, but using defineProperty code>What problems will occur?

Give me an example:

Define the attribute hobbies of data as the array ['swimming', 'football']. When an element is inserted at the front of the array running , see what happened?

const data = {<!-- -->
  name: 'zhangsan',
  age: 19,
  address: {<!-- -->
    city: 'beijing',
    country: 'China',
  },
  hobbies: ['swimming', 'football'],
}
observer(data)
data.hobbies.unshift('running')
console.log(data.hobbies)

Output result:

First of all, when an element is inserted into the first position of the array, two view updates are triggered. This is because when monitoring the hobbies array, the 0 of the array is changed. ,1The two keys are monitored separately, so when a value is inserted at the front of the array, all the values corresponding to the original index have changed, so two triggers are triggered. View updates. Although Vue adopts an asynchronous update strategy, these two updates will be merged and have little impact. But if the amount of data in the array is very large or the elements of the array are deep objects, the performance loss will be huge.

Secondly, we can see that the third element football of the updated hobbies is not responsive. This is because the array is calling observerWhen the function implements responsive monitoring, there are only two indexes: 0 and 1, so only these two key are converted into Listening is performed in the form of getter/setter, so no matter how many elements are added to the subsequent array, it is no longer responsive.

Therefore, if you use defineProperty to monitor array elements, you will not be able to monitor changes in new elements. Many APIs that operate on arrays cannot be used, and will trigger multiple This is not a good strategy.

? Just to interrupt, if the element value is modified through the index at this time, it can actually be monitored.

const data = {<!-- -->
  name: 'zhangsan',
  age: 19,
  address: {<!-- -->
    city: 'beijing',
    country: 'China',
  },
  hobbies: ['swimming', 'football'],
}
observer(data)
data.hobbies[0] = 'running'
console.log(data.hobbies)

Output result:


In Vue2.0, the author uses seven methods to rewrite the array in place to monitor the array data.

1. Array method interceptor

In Vue2.0, an array method interceptor is defined, intercepted between the array instance and Array.property, and the method of operating the array is rewritten inside the interceptor For some methods, when an array instance uses an array operation method, the methods rewritten in the interceptor are used instead of the native methods on Array.prototype.

//Source code location:/src/core/observer/array.ts
const arrayProto = Array.prototype
//Create an object as an interceptor
 export const arrayMethods = Object.create(arrayProto)
// 7 ways to change the contents of the array itself
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
]

/**
 * @description: Define and override 7 methods on the arrayMethods interceptor object
 * @return {*}
 */
methodsToPatch.forEach((method) => {<!-- -->
  const original = arrayProto[method] // cache native method
  Object.defineProperty(arrayMethods, method, {<!-- -->
    enumerable: false,
    writable: true,
    configurable: true,
    value: function mutator(...args) {<!-- -->
      const result = original.apply(this, args)
      const ob = this.__ob__
      // If an element is inserted or updated, monitor the new element responsively
      let inserted
      switch (method) {<!-- -->
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          inserted = args.slice(2)
          break
      }
      if (inserted) ob.observeArray(inserted) // Call the observe function to convert the new elements into responsive
      console.log(
        `Someone has called the overridden ${<!-- -->method} method to change the array data, and the view can be notified to be updated`
      )
      updateView()
      return result
    },
  })
})

const array = [1, 2]
Object.setPrototypeOf(array, arrayMethods)
array.push(3)
console.log(array)

Output result:


2. Use interceptors

Determine the type of value in the Observer class. If it is an array, point the __proto__ pointer of value to the interceptor. At the same time, call the observeArray() function to monitor value

/**
 * @description: Define the Observer class to convert all properties of an object into observable objects
 * @return {*}
 */
class Observer {<!-- -->
  constructor(value) {<!-- -->
    // ...
    if (Array.isArray(value)) {<!-- -->
      // setPrototypeOf is a new syntax in es6
      // You can also determine whether the browser supports __proto__ syntax. If it supports value.__proto__ = arrayMethods
      // Otherwise, traverse the methods in arrayMethods and add them to the value object itself one by one.
      Object.setPrototypeOf(value, arrayMethods)
      this.observeArray(value) // Convert all elements in the array into detectable responses
    } else {<!-- -->
      this.walk(value)
    }
  }
  observeArray(value) {<!-- -->
    for (let i = 0, l = value.length; i < l; i + + ) {<!-- -->
      observer(value[i])
    }
  }
}

const data = {<!-- -->
  name: 'zhangsan',
  age: 19,
  address: {<!-- -->
    city: 'beijing',
    country: 'China',
  },
  hobbies: ['swimming', 'football'],
}
observer(data)

data.hobbies.unshift('running')
console.log('--------------------------------------------- --------')
console.log(data.hobbies)
console.log('--------------------------------------------- --------')
data.hobbies[0] = 'drawing'
console.log('--------------------------------------------- --------')
console.log(data.hobbies)

Output result:

The observer function implements monitoring of seven operation methods of arrays. At this time, modification of array elements through index cannot be monitored, and splice can be used instead.

At this point, we have implemented data monitoring of objects and arrays, solving the first problem of responsiveness:

  • How to change stateListen for changes?

So after monitoring the status changes, how does vue know which views to update?

2. Collection and triggering of dependencies

During the rendering process, the vue component will call the getter function of the data to read the data and render it on the page. Then when the data is read, the The caller collects it, and when the data changes, that is, when the setter function is triggered, all callers are notified to update the view, thus realizing responsive updating of the data.

We call the caller of data a dependency of data.

The core is: Collect dependencies in getter and trigger dependencies in setter


During the template compilation process, a watcher instance will be instantiated for each element that calls state;

During the construction process, this watcher instance first sets itself to the globally unique specified location window.target, and then reads the dependent data. Reading data triggers the getter of the data. In the getter function, the value of window.target will be read to obtain the dependencies of the current data. watcher, then call dep.depend() to collect this watcher into the dependency collector dep.subs Completed dependency collection;

When the data changes, the setter function of the data will be called. In the setter, dep.notify() will be called to notify the collector dep. Each watcher instance in subs sends an update notification. After receiving the notification, each watcher instance will call its own update The function updates the view, thus completing the triggering of dependencies and achieving data responsiveness.

1. Define Dep class

// Source code location: src/core/observer/dep.js
/**
 * @description: Define Dep class
 * @return {*}
 */
class Dep {<!-- -->
  constructor() {<!-- -->
    this.subs = [] // Internally stores all watcher instances that observe the dependent data
  }
  /**
   * @description: Notification update function
   * @return {*}
   */
  notify() {<!-- -->
    this.subs.forEach((sub) => {<!-- -->
      sub.update() //update method on watcher instance
    })
  }
  /**
   * @description: Dependency collection function
   * @return {*}
   */
  depend() {<!-- -->
    if (window.target) {<!-- -->
      //When the watcher calls the dependency getter, it will place itself on window.target for the dependency collector to obtain, and delete it after reading it.
      this.addSub(window.target)
    }
  }
  /**
   * @description: Add a dependency to the subs array
   * @param {*} sub a watcher instance
   * @return {*}
   */
  addSub(sub) {<!-- -->
    this.subs.push(sub)
  }
  /**
   * @description: Remove a dependency from the subs array
   * @param {*} sub a watcher instance
   * @return {*}
   */
  removeSub(sub) {<!-- -->
    if (this.subs.length) {<!-- -->
      const index = this.subs.indexOf(sub)
      if (index > -1) {<!-- -->
        return this.subs.splice(index, 1)
      }
    }
  }
}

2. Define Watcher class

// Source code location: src/core/observer/watcher.js
/**
 * @description: Define Watcher class
 * @return {*}
 */
class Watcher {<!-- -->
  constructor(vm, expOrFn, cb) {<!-- -->
    This.vm = vm
    this.cb = cb
    this.getter = parsePath(expOrFn) // Function to get the object attribute value based on expression
    this.value = this.get() // Directly call and execute during construction
  }
  /**
   * @description: Data acquisition of watcher instance
   * @return {*}
   */
  get() {<!-- -->
    window.target = this // Mount itself to window.target first
    let value = this.getter.call(this.vm) // Read dependency data and trigger dependency collection
    window.target = null // After the dependencies are collected, window.target will be empty.
    return value //The value of the obtained data is used for page rendering
  }
  /**
   * @description: View update of watcher instance
   * @return {*}
   */
  update() {<!-- -->
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue) // Comparison of new and old virtual DOM or user-defined callback function, etc.
  }
}

/**
 * Parse simple path.
 * Take the value represented by a string path in the form of 'data.a.b.c' from the real data object
 * For example:
 * data = {a:{b:{c:2}}}
 * parsePath('a.b.c')(data) // 2
 */
function parsePath(path) {<!-- -->
  const segments = path.split('.')
  return function (obj) {<!-- -->
    for (let i = 0, len = segments.length; i < len; i + + ) {<!-- -->
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

3. Collect & trigger dependencies

/**
 * @description: Define responsiveness for the object's properties, which can be monitored for read events of the object's properties.
 */
function defineReactive(obj, key, value) {<!-- -->
  // ...
  let childOb = observer(value)
  const dep = new Dep() // Instantiate dependency collector
  Object.defineProperty(obj, key, {<!-- -->
    get() {<!-- -->
      dep.depend() // Collect dependencies
      if(childOb){<!-- --> // If the child element of the current value is responsive, it also needs to collect dependencies.
        childOb.dep.depend()
      }
      return value
    },
    set(newValue) {<!-- -->
      if (newValue !== value) {<!-- -->
        chileOb = observer(newValue) //The updated value must also be monitored
        value = newValue
        dep.notify() // Trigger dependencies and notify updated views
        if (childOb) {<!-- --> // If the child element of the current value is responsive, it also needs to be notified and updated.
          childOb.dep.notify()
        }
      }
    },
  })
  return dep
}

At this point, we have solved the second problem of responsive implementation:

  • How to determine stateViews to be updated after changes?

3. Asynchronous update queue

Vue executes asynchronously when updating DOM. As long as it listens to data changes, Vue will open a queue and buffer all data changes that occur in the same event loop.

If the same watcher is triggered multiple times in the same eventLoop, it will only be pushed into the queue once.

Then, in the next event loop tick, Vue flushes the queue and performs the actual (deduplicated) work.

Vue internally attempts to use native Promise.then(), MutationObserver() and setImmediate() for asynchronous queues >, if the execution environment does not support it, setTimeout() will be used instead.

If you want to operate based on the updated DOM, you should use the Vue.nextTick() function. The callback function of this function will be called after the page is refreshed.

The third problem is also solved:

  • When is the time to update the view?

The above is the whole process of data-driven view!

4. Method of modifying data responsively

Since Vue2.0 uses Object.defineProperty provided by javaScript to implement data monitoring

Disadvantage:

  • Object:
    1. Add a new key/value key-value pair
    2. Delete existing key/value key-value pairs
  • Array:
    1. Modify array element value by index
    2. Modify array length

Solution:

  • object

    1. Vue.set(object, key, value) or vm.$set(object, key, value) to add and modify properties
    2. Vue.delete(object, key) or vm.$delete(object, key) delete attributes
    3. object = Object.assign({}, newObject)Updates the entire object
  • array

    1. array.splice(index, 1, value)
    2. Vue.set(array, index, value) or vm.$set(array, index, value)
    3. array = newArray updates the entire array

Vue3.0 uses the new Proxy of ES6 for data monitoring, and does not have the above problems.

5. Summary

Vue2.0 adopts data hijacking and message publishing and subscription models to achieve data responsiveness.

When a Vue instance is created, Vue will traverse the data in data and convert them using Object.defineProperty It is getter/setter, and internally tracks related dependencies (message subscriptions), captures data access events and collects dependencies, captures data modification events and triggers dependencies.

Each component instance has a corresponding watcher instance, which records the properties as dependencies during component rendering. When the dependent data's setter is called, it will Notify watcher to update the page (message publishing), thereby achieving "data-driven view".
https://img-blog.csdnimg.cn/img_convert/6c4d1492d21556788d4847 472788440e.png

syntaxbug.com © 2021 All Rights Reserved.