SoFunction
Updated on 2025-04-05

Vue source code learning about data listening implementation on Array

summary

We all know that Vue's responsiveness is to perform data hijacking. But that can be implemented for Object types. What if it is an array? It is not possible to use set/get method.

But the Vue authors used a way to implement Array type monitoring: an interceptor.

Core idea

Overwrite the prototype object of the array itself by creating an interceptor.

Interceptor

Check the Vue source code path vue/src/core/observer/.

/**
  * Vue's change detection of arrays
  * Thought: Overwrite with an interceptor.
  * Interceptor is actually an Object, and its properties are the same.  It only processes the variation method of the array.
 */

function def (obj, key, val, enumerable) {
  (obj, key, {
   value: val,
   enumerable: !!enumerable,
   writable: true,
   configurable: true
  })
}

// Array prototype objectconst arrayProto = 
// Interceptorconst arrayMethods = (arrayProto)

// Mutation array method: The method that changes the original array after executionconst methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

(function (method) {
  // Methods on cached original array prototypes  const original = arrayProto[method]
  // Process each array compilation method (intercept)  def(arrayMethods, method, function mutator (...args) {
   // The returned value is still the result of execution through the array prototype method itself   const result = (this, args)
   // When each value is observer(), it will be marked with a __ob__ attribute   const ob = this.__ob__
   // Store the array that calls to execute the mutated array method to cause the value of the array itself to change, mainly referring to the part of the original array that increases (re-Observer is required)   let inserted
   switch (method) {
    case 'push':
    case 'unshift':
     inserted = args
     break
    case 'splice':
     inserted = (2)
     break
   }
   //Re-Observe newly added array elements   if (inserted) (inserted)
   // Send change notification   ()
   return result
  })
})

About when to use Observer on data properties in Vue

If you are familiar with Vue source code, you should be able to find the Vue entry file vue/src/core/instance/ soon.

function Vue (options) {
 if (.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 proxies attribute $props, $data//Bind three instance methods to Vue prototype: vm.$watch, vm.$set, vm.$deletestateMixin(Vue)
// Example methods related to binding events to Vue prototype: vm.$on, vm.$once, vm.$off, vm.$emiteventsMixin(Vue)
// Bind Vue prototype life cycle related instance methods: vm.$forceUpdate, and private method_updatelifecycleMixin(Vue)
// Bind Vue prototype life cycle related instance methods: vm.$nextTick, and private method_render, and a bunch of tool methodsrenderMixin(Vue)

export default Vue

()

Source code path: vue/src/core/instance/.

export function initMixin (Vue: Class<Component>) {
 ._init = function (options?: Object) {
  // Current instance  const vm: Component = this
  // a uid
  // Instance unique identifier  vm._uid = uid++

  let startTag, endTag
  /* istanbul ignore if */
  // Development mode, enable Vue performance detection and API-enabled browsers.  if (.NODE_ENV !== 'production' &&  && mark) {
   startTag = `vue-perf-start:${vm._uid}`
   endTag = `vue-perf-end:${vm._uid}`
   // Start placing in the component initialization stage   mark(startTag)
  }

  // a flag to avoid this being observed
  // Identified as a Vue instance  vm._isVue = true
  // merge options
  // Pass the optionsMerge 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(),
    options || {},
    vm
   )
  }
  /* istanbul ignore else */
  if (.NODE_ENV !== 'production') {
   initProxy(vm)
  } else {
   vm._renderProxy = vm
  }
  // expose real self
  vm._self = vm
  // Initialize life cycle  initLifecycle(vm)
  // Initialize event center  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  // Initialize State  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')

  /* istanbul ignore if */
  if (.NODE_ENV !== 'production' &&  && mark) {
   vm._name = formatComponentName(vm, false)
   mark(endTag)
   measure(`vue ${vm._name} init`, startTag, endTag)
  }
  // Mount  if (vm.$) {
   vm.$mount(vm.$)
  }
 }
}

initState()

Source code path: vue/src/core/instance/.

export function initState (vm: Component) {
 vm._watchers = []
 const opts = vm.$options
 if () initProps(vm, )
 if () initMethods(vm, )
 if () {
  initData(vm)
 } else {
  observe(vm._data = {}, true /* asRootData */)
 }
 if () initComputed(vm, )
 if ( &&  !== nativeWatch) {
  initWatch(vm, )
 }
}

At this time you will find that the observe appears.

observe

Source code path: vue/src/core/observer/

export function observe (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) {
  // The value is already a responsive data and no Observe instance is created to avoid repeated listening  ob = value.__ob__
 } else if (
  shouldObserve &&
  !isServerRendering() &&
  ((value) || isPlainObject(value)) &&
  (value) &&
  !value._isVue
 ) {
  // The target appears, create an Observer instance  ob = new Observer(value)
 }
 if (asRootData && ob) {
  ++
 }
 return ob
}

Time to use interceptor

There is an Observe class in Vue's responsive system. Source code path: vue/src/core/observer/.

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

const arrayKeys = (arrayMethods)

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

function copyAugment (target: Object, src: Object, keys: Array<string>) {
 // target: Objects that need to be Observed // src: Array proxy prototype object // keys: const arrayKeys = (arrayMethods)
 // keys: Several compilation method names on the array proxy prototype object // const methodsToPatch = [
 //  'push',
 //  'pop',
 //  'shift',
 //  'unshift',
 //  'splice',
 //  'sort',
 //  'reverse'
 // ]
 for (let i = 0, l = ; 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) {
   = value
  // 
   = new Dep()
   = 0
  def(value, '__ob__', this)
  // If it is an array  if ((value)) {
   if (hasProto) {
    // If __proto__ attribute (non-standard attribute, supported by most browsers): Directly point the prototype to the proxy prototype object    protoAugment(value, arrayMethods)
   } else {
    // It is not supported to mount the processed variant method of the same name on the array instance (and cannot be enumerated) to intercept prototype object method    // When you access an object method, you will only look up on the prototype object when it does not exist.    copyAugment(value, arrayMethods, arrayKeys)
   }
   (value)
  } else {
   (value)
  }
 }

 /**
  * Walk through all properties and convert them into
  * getter/setters. This method should only be called when
  * value type is Object.
  */
 walk (obj: Object) {
  const keys = (obj)
  for (let i = 0; i < ; i++) {
   defineReactive(obj, keys[i])
  }
 }

 /**
   * Iterate through each item in the array to listen for changes, that is, each element executes Observer() once.
   */
 observeArray (items: Array<any>) {
  for (let i = 0, l = ; i < l; i++) {
   observe(items[i])
  }
 }
}

How to collect dependencies

The real data responsive processing in Vue is defineReactive(). defineReactive method is to convert the object's data attribute into the accessor attribute, that is, to set get/set for the data attribute.

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


export function defineReactive (
 obj: Object,
 key: string,
 val: any,
 customSetter?: ?Function,
 shallow?: boolean
) {
 // dep is used in the accessor attribute closure // Each data field refers to its own dep constant through a closure // Dep objects for each field are used to collect those dependencies that belong to the corresponding field. const dep = new Dep()

 // Get the property description object that may already exist in this field const property = (obj, key)
 // Boundary situation handling: An unconfigurable property cannot be used and there is no need to use it to change its attribute definition. if (property &&  === false) {
  return
 }

 // Since an object's property is likely to be an accessor property, it is likely that the property already has a get or set method // If the function is used next to redefine the setter/getter of the property // This will cause the original set and get methods of the attribute to be overwritten, so the original setter/getter of the attribute to be cached const getter = property && 
 const setter = property && 
 // Boundary situation handling if ((!getter || setter) &&  === 2) {
  val = obj[key]
 }
 // The default is depth observation, referring to the sub-properties __ob__ // Provides trigger dependencies for the or method. let childOb = !shallow && observe(val)
 (obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
   // If getter exists, then call the function directly and use the return value of the function as the value of the attribute to ensure that the original reading operation of the attribute is normal   // If getter does not exist, use val as the value of the property   const value = getter ? (obj) : val
   // The value is assigned when instantiating the Watch   if () {
    // Start collecting dependencies to dep    ()
    if (childOb) {
     ()
     if ((value)) {
      // Call the dependArray function to trigger the dependency collection of each element of the array one by one      dependArray(value)
     }
    }
   }
   // Return the property value correctly.   return value
  },
  set: function reactiveSetter (newVal) {
   // Get the original value   const value = getter ? (obj) : val
   /* eslint-disable no-self-compare */
   // Compare whether the old and new values ​​are equal, consider the NaN situation   if (newVal === value || (newVal !== newVal && value !== value)) {
    return
   }
   /* eslint-enable no-self-compare */
   if (.NODE_ENV !== 'production' && customSetter) {
    customSetter()
   }
   // #7981: for accessor properties without setter
   if (getter && !setter) return
   // If there is a setter before the data, then you should continue to use this function to set the value of the property   if (setter) {
    (obj, newVal)
   } else {
    // Assign new values    val = newVal
   }
   // Since the property is set a new value, then if the new value we set for the property is an array or a pure object,   // Then the array or pure object is not observed, so the new value needs to be observed   childOb = !shallow && observe(newVal)
   // Notify the watcher update in dep   ()
  }
 })
}

List of array dependencies

Why do we need to place dependencies on Observer instances? Right now

export class Observer {
  constructor (value: any) {
    ...
     = new Dep()
  }
}

First we need to access the Observer instance in getter

// That is, the abovelet childOb = !shallow && observe(val)
...
if (childOb) {
 // Call the depend() method of dep on the Observer instance to collect dependencies ()
 if ((value)) {
  // Call the dependArray function to trigger the dependency collection of each element of the array one by one  dependArray(value)
 }
}

In addition, we need to use the Observer instance in the interceptor mentioned above.

(function (method) {
  ...
  // this indicates the data currently being operated  // But how did __ob__ come from?  const ob = this.__ob__
  ...
  //Re-Observe newly added array elements  if (inserted) (inserted)
  // Send change notification  ()
  ...
})

Think about where the above-mentioned this.__ob__ attribute comes from?

export class Observer {
  constructor () {
    ...
     = new Dep()
    // Add a non-enumerable __ob__ attribute on vue, the value of this attribute is the Observer instance    // Therefore, we can obtain the Observer instance through array data __ob__    // Then get the dep on __ob__    def(value, '__ob__', this)
    ...
  }
}

Remember that once all attributes are detected, they will be marked with a __ob__ tag, which means they are responsive data.

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.