Response principle

According to Vue’s official description, Vue’s responsive system is non-invasive. How does Vue convert different types of data (basic types, ordinary objects, arrays, etc.) into detectable data? Before we look at the actual implementation of Vue, let’s look at why we want to make data testable.

Because Vue is the MVVM framework, data drives the view. In traditional development (non-data-driven views), we need to manipulate the DOM to update the view, but in Vue, the view is data-driven, which means we only need to manipulate the data, and the Vue does the DOM manipulation for us. This way, our development work is much easier, we only need to maintain the data, and do not need to do DOM manipulation. One of the core aspects of data-driven views is to detect changes in the data, and when the data changes, you need to update the view.

Coming up, we return to our first question: how does Vue manage to convert different types of data (primitives, plain objects, arrays, and so on) into something testable?

Responsive object

In the initialization of the Vue, executes initState method, which are defined in the file: SRC/core/instance/state. Js.

function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

InitState is used to initialize props, methods, data, computed, and watch.

initProps

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options. PropsData | | {} const props. = the vm _props = {} / / cache key prop, prop update for future can use Array rather than enumeration iterative dynamic object key. const keys = vm.$options._propKeys = [] const isRoot = ! vm.$parent// root instance props should be converted on line // root instance props should be converted on lineif(! isRoot) { toggleObserving(false)}for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else* /if(process.env.NODE_ENV ! = ='production'{/ /... }else{defineReactive(props, key, value)} // During vue.extend (), static props have propped on the prototype of the component // We just need the propped definition instantiation here.if(! (keyin vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)}Copy the code

InitProps basically iterate over the propsOptions (that is, the props property we defined in the Vue component) and then do two things: define Active and Proxy.

initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if(! isPlainObject(data)) { data = {} process.env.NODE_ENV ! = ='production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function'Keys (data) const props = vm$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if(process.env.NODE_ENV ! = ='production') {// Check if there is a key with the same name on methodsif (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if(props && hasOwn(props, key)) {// Check if there is a key of the same name on the props. = ='production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if(! IsReserved (key)) {proxy(vm, '_data', key)}}true /* asRootData */)
}
Copy the code

InitData does the following:

  • Data can be a function; if so, the return value of the function is treated as data
  • Iterate over the keys on data
    • Check if each key is already defined on methods and props, because data/methods/props keys are propped to the VM and cannot have the same name
    • Proxies data’s key to the VM
    • Observe data

Both props and data are initialized using proxy methods and observe methods. What do these methods do? Let’s take a look.

proxy

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code

The logic of proxy is relatively simple, that is, to directly proxy the key in the sourceKey on target to target, that is, through defineProperty, Change the method of obtaining and setting by sourceKey to the method of obtaining and setting by target. The core purpose of proxy is to directly delegate props/data to the VM instance, so that we can get the data on the props/data directly in the Vue component through vm. XXX /this. XXX.

defineReactive

DefineReactive methods defined in the file: SRC/core/observer/index. Js

functiondefineReactive ( obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key)if (property && property.configurable === false) {
    return} // Caters to the predefined getter/setter const getter = property && property.getif(! getter && arguments.length === 2) { val = obj[key] } const setter = property && property.setletchildOb = ! shallow && observe(val) Object.defineProperty(obj, key, { enumerable:true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify() } }) }Copy the code

DefineReactive is the key to turning data into responsiveness, mainly using the Object.defineProperty method, which can be viewed in the MDN documentation if you are not familiar with.

DefineReactive basically does is redefine getters and setters for a key of an object, add collect dependent operations on top of the getter, and add update operations on top of the setter. This way, when we fetch the value of the data, we fire its getter, and then collect the subscribers that use the value; When we reassign a value, the setter is triggered to notify previously collected subscriber data of the change, so that a key on the object can be turned into a responsive form by data hijacking.

Prior to this, let childOb =! Shallow && Observe (val), what does the observe method do?

observe

Observe methods defined in the file: SRC/core/observer/index. Js.

functionobserve (value: any, asRootData: ? boolean): Observer | void {if(! isObject(value) || value instanceof VNode) {return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && ! value._isVue ) { ob = new Observer(value) }if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

Observe basically creates an Observer instance and returns it. What kind of Observer is that? Defined in the file: SRC/core/observer/index. Js

// The observer class attached to each observed object. // After attaching, the observer converts the property key of the target object into a getter/setter that collects dependencies and dispatches updates. class Observer { value: any; dep: Dep; vmCount: number; // Use this object as the root$dataConstructor (constructor: any) {this.value = value this.dep = new dep () this.vmCount = 0 def(value, constructor: any) {this.value = value this.dep = new dep () this.vmCount = 0 def(value, constructor: any) {this.value = value this.'__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else{this.walk(value)}} // iterate over each property and convert them to getters/setters. This method should only be called if the value type is "object". walk (obj: Object) { const keys = Object.keys(obj)for (leti = 0; i < keys.length; I ++) {defineReactive(obj, keys[I])}} observeArray (items: Array<any>) {for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
Copy the code

The Observer logic is clear: instantiate Dep, then def(value, ‘__ob__’, this), observe each entry if value is an array, and defineReactive all keys if value is an object.

functiondef (obj: Object, key: string, val: any, enumerable? : boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !! enumerable, writable:true,
    configurable: true})}Copy the code

Def mounts an Observer instance to the __ob__ attribute of value.

Observe also calls each other in observe and defineReactive. What’s the difference between observe and defineReactive? Observe converts an object or array to reactive and defineReactive converts a key-value pair to reactive. Both internally call each other. In defineReactive, if the value of an attribute is an object, it is converted to reactive by Observe. Observe iterates through each property of the object and converts each property to reactive by defineReactive. Let’s take an example:

data() {return {
    time: "The 2020-02-22 15:01:30",
    person: {
      name: 'haha', age: 18,}, list: ['1'.'2']}}Copy the code

Vue initializes through initData and observes the entire object returned by data. Since data is an object, it traverses all key/value pairs of the object through walk and defineReactive. This is to convert the time/person/list keys into a responsive form. When the values of the three keys are changed, the corresponding subscribers will be notified. In defineReactive, the person/list values are observed because they are not primitive types. In this recursive way, the entire object will be a responsive object no matter how many layers an object has nested and what type its sub-attributes are.

Precautions for detecting changes

Vue cannot detect data changes in the following cases:

  • Vue cannot detect the addition or deletion of object attributes
  • Vue cannot detect the following array changes:
    • When an item is set directly with an index, for example, vm.items[index] = newValue
    • When changing the length of an array, for example, vm.items.length = newLength

So, Vue does something to make the objects and data that you manipulate responsive.

Vue.set

Set methods defined in the file: SRC/core/observer/index. Js

// Set properties on the object. Adds a new property, triggering a change notification if the property does not already exist.function set (target: Array<any> | Object, key: any, val: any): any {
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key intarget && ! (keyin Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if(! ob) { target[key] = valreturn val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
Copy the code

The main logic of the set method is:

  • If target is an array, set the length of the array and replace the elements with splice
  • If target is an object, if the key exists in itself, it is assigned because the key is already responsive; If key does not exist and ob does not exist, the value is directly assigned and returned, because the object is not responsive. If key does not exist and OB does exist, passdefineReactiveBy setting this key to be responsive and manually distributing updates, the newly set properties become responsive as well.

So why can arrays just go through Splice and tell the view to update?

An array of

This is because Vue makes changes to the native methods of the array that trigger the dispatch of updates.

class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else{/ /... }}}Copy the code

In an Observer, augment and this.observeArray are executed if the value is an array.

// Enhance the target object or array by intercepting the prototype chain with __proto__functionprotoAugment (target, src: Object, keys: Any) {/* eslint-disable no-proto */ target.__proto__ = SRC /* eslint-enable no-proto */} // Enhance the target object or array by defining hidden properties. /* istanbul ignore next */function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
Copy the code

Therefore, the main function of Augment is to point the value prototype to arrayMethods. ArrayMethods are defined in the file: SRC/core/observer/array. Js.

import { def } from '.. /util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'] // Intercepts the modification method and raises the event methodStopatch.foreach (function(method) {const original = arrayProto[method] def(arrayMethods, method,functionmutator (... args) { const result = original.apply(this, args) const ob = this.__ob__let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
Copy the code

ArrayMethods is array. prototype and overrides Array methods. These methods, in addition to performing the original logic, observe the new element in the array, and finally manually trigger the distribution of the update, so that the array can also trigger the view update through the array method.

When did Observe’s DEP collect dependencies? The answer is in defineReactive

functiondefineReactive ( obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean ) { // ...letchildOb = ! shallow && observe(val) Object.defineProperty(obj, key, { enumerable:true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      returnvalue }, // ... })}Copy the code

When childOb is present, executing childob.dep.depend () will do the collection of dependencies, so when ob.dep.notify() is triggered, an update will be dispatched. If value is an array, then collect each element of the array by dependArray.