Abstract

We all know that Vue responds by hijacking data via Object.defineProperty. But that can be done for Object, what about arrays? Set /get does not work.

But the Vue authors use a way to implement array-type monitoring: interceptors.

core idea

Create an interceptor to override the array.prototype object of the Array itself.

The interceptor

By looking at the Vue source path Vue/SRC/core/observer/array. Js.

/** * Idea: override array. prototype with an interceptor. * The interceptor is actually an Object with the same properties as Array.prototype. It's just the array mutation method. * /functiondef (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !! enumerable, writable:true,
      configurable: true// arrayProto = array.prototype // arrayProto = array.prototype // arrayMethods = object.create (arrayProto) // arrayProto = array.create (arrayProto) Const methodsToPatch = [method that changes the original array upon execution'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

methodsToPatch.forEach(functionConst original = arrayProto[method] const original = arrayProto[method] def(arrayMethods, method,functionmutator (... Const result = original.apply(this, const result = original. Const ob = this.__ob__ // Store the array that changes the value of the array when the variable array method is called. Mainly refers to the part of the original array that is added (reobserver)let inserted
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          inserted = args.slice(2)
          break} // Reobserve the new array elementif(inserted) ob.observearray (inserted) // Send change notification ob.dep.notify()return result
    })
})

Copy the code

When does Vue Observer the data property

If you are familiar with the Vue source of children’s shoes should be quick to find the entrance to the Vue file Vue/SRC/core/instance/index, js.

function Vue (options) {
  if(process.env.NODE_ENV ! = ='production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')} this._init(options)} initMixin(Vue) // Bind the proxy properties to the prototype$props.$data// Bind three instance methods to the Vue prototype: VM.$watch, the vm.$set, the vm.$deleteStateMixin (Vue) // Bind the Vue prototype to the event-related instance method: VM.$on, vm.$once ,vm.$off , vm.$emitEventsMixin (Vue) // Bind the Vue prototype to a life-cycle dependent instance method: VM.$forceUpdate, vm.destroy, and the private method _update lifecycleMixin(Vue) // bind the Vue prototype to the life-cycle related instance method: VM.$nextTickAnd the private _render method, and a bunch of utility methods called renderMixin(Vue)export default Vue
Copy the code

this.init(a)

Source path: vue/SRC/core/instance/init. Js.


export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function(options? : Object) {// Current instance const vm: Component = this // a uid // Instance unique identifier vm._uid = ID ++let startTag, endTag
    /* istanbul ignore if*/ // In development mode, enable Vue performance detection and support performance. Mark API browser.if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}'// Mark (startTag)} // a flag to avoid this being observed // Marks a Vue instance vm._isVue =trueMerge options // Merge the optionsMerge we passed into$options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else* /if(process.env.NODE_ENV ! = ='production') {
      initProxy(vm)
    } else{vm._renderProxy = vm} // expose Real self vm._self = vm // initRender(vm) callHook(vm,'beforeCreate'// be wary when using the initState injection (vm) // be wary when using the initState injection (VM) // be wary when using the initState injection (VM) // be wary when using the initProvide(VM) injection provide after data/props callHook(vm,'created')

    /* istanbul ignore if* /if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name}Init ', startTag, endTag)} // Mountif (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
Copy the code

initState()

Source path: vue/SRC/core/instance/state. Js.

export 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

And you’ll see Observe come on.

observe

Source path: vue/SRC/core/observer/index. Js

export functionobserve (value: any, asRootData: ? boolean): Observer | void {if(! isObject(value) || value instanceof VNode) {return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__'Ob = value.__ob__ instanceof Observer) {ob = value.__ob__ instanceof Observer.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

The timing of using interceptors

Vue has an Observe class in its responsive system. Source path: vue/SRC/core/observer/index. Js.

// can we use __proto__?
export const hasProto = '__proto__' in {}

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

functionCopyAugment (target: Object, SRC: Object, keys: Array<string>) {// Target: Object to be observed // SRC: Array proxy prototype Object // keys: Const arrayKeys = Object. GetOwnPropertyNames (arrayMethods) / / keys: array agent on the prototype Object of a few compilation method name / / const methodsToPatch = [/ /'push', / /'pop', / /'shift', / /'unshift', / /'splice', / /'sort', / /'reverse'
  // ]
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    // 
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this) // If it is an arrayif (Array.isArray(value)) {
      if(hasProto) {// If the __proto__ attribute is supported (non-standard attribute, most browsers support): direct the prototype to the proxy prototype object protoAugment(value, arrayMethods)}else{// Augment(value, value, value, value, value) {// Augment(value, value, value, value) {// Augment(value, value, value) arrayMethods, arrayKeys) } this.observeArray(value) }else{ this.walk(value) } } /** * Walk through all properties and convert them into * getter/setters. This method should only  be called when * valuetype is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (leti = 0; i < keys.length; I ++) {defineReactive(obj, keys[I])}} /** * perform Observer() */ observeArray (items: Array<any>) {for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
Copy the code

How to collect dependencies

What really does reactive data processing in Vue is defineReactive(). The defineReactive method converts the data attributes of an object to accessor attributes by setting get/set for the data attributes.

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}


export functiondefineReactive ( obj: Object, key: string, val: any, customSetter? :? Function, shallow? : Boolean) {// dep in accessor properties closure use // each data field refers to its own DEP constant through the closure // The DEP object for each field is used to collect the dependencies belonging to the corresponding field. Const dep = new dep () / / get the field description Object might have been const property = Object. GetOwnPropertyDescriptor (obj, key) / / boundary condition processing: An unconfigurable property cannot and does not need to change its property definition using Object.defineProperty.if (property && property.configurable === false) {
    return} // Since the property of an object is probably already an accessor property, it is likely that the property already exists get orsetMethod // If the setter/getter for the property is then redefined using object.defineProperty, this will result in the property's originalsetAnd get methods are overridden, Const getter = property && property.get const setter = property && property.set // boundary case handlerif((! Getter | | setter) && the arguments. Length = = = 2) {val = obj} [key] / / the default is the depth of observation, An __ob__ // that references a child attribute provides triggering dependencies for vue. set or vue. delete methods.letchildOb = ! shallow && observe(val) Object.defineProperty(obj, key, { enumerable:true,
    configurable: true,
    get: function reactiveGetterConst value = getter? Const value = getter? Const value = getter? Const value = getter? Const value = getter? Getter.call (obj) : The value of val // dep. target is assigned when the Watch is instantiatedif(dep.target) {// Start collecting dependencies to Dep dep.depend()if (childOb) {
          childOb.dep.depend()
          if(array.isarray (value)) {// Invoke the dependArray function on each element of the Array individually to collect the dependArray(value)}}} // Return the attribute value correctly.return value
    },
    set: functionReactiveSetter (newVal) {// Get the original value const value = getter? Getter. call(obj) : val /* eslint-disable no-self-compare */ / compares old and new values to be equal, considering NaN casesif(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if(getter && ! setter)return// If there is a setter before the data, you should continue to use that function to set the value of the propertyif (setter) {
        setter.call(obj, newVal)
      } elseChildOb =! ChildOb =! ChildOb =! ChildOb =! ChildOb =! Shallow && observe(newVal) // Notify the watcher in deP to update dep.notify()}})}Copy the code

Store a list of array dependencies

Why do we need to store dependencies on Observer instances? namely

export class Observer {
    constructor (value: any) {
        ...
        this.dep = new Dep()
    }
}
Copy the code

First we need to access the Observer instance in the getter

// That is the aboveletchildOb = ! shallow && observe(val) ...if(childOb) {// Call the depend() method of dep on an Observer instance to collect dependencies childob.dep.depend ().if(array.isarray (value)) {// Invoke the dependArray function on each element of the Array to collect the dependArray(value)}}Copy the code

In addition, we use an Observer instance in the interceptor mentioned earlier.

methodsToPatch.forEach(function(method) { ... // This represents the data currently being manipulated // but __ob__ comes from? const ob = this.__ob__ ... // Reobserve the new array elementif(inserted) ob.observearray (inserted) // Send change notification ob.dep.notify()... })Copy the code

Where does the this.__ob__ attribute come from?

export class Observer {
    constructor() {... This.dep = new dep () // Add an unenumerable __ob__ attribute to vue, The value of this attribute is the Observer instance // so we can get the Observer instance from the array __ob__ // and then get the dep def(value, on __ob__)'__ob__', this) ... }}Copy the code

Remember that all attributes, when detected, are marked with an __ob__, indicating that they are responsive data.

Note about Array

Due to JavaScript limitations, Vue cannot detect the following altered arrays:

  • When you set an item directly using an index, for example, vm.items[indexOfItem] = newValue
  • When you modify the length of an array, for example, vm.items. Length = newLength

The solution is as follows:

Note about Array