Analysis of Vue responsive principle source code

Keywords: Javascript Front-end Vue Vue.js source code

Let's look at the picture first to understand the general process and what to do

initialization

During the initialization of new Vue, the data props and data of our components will be initialized. Since this article mainly introduces the response type, I won't explain it too much. Let's take a look at the source code

Source address: Src / core / instance / init.js - line 15

export function initMixin (Vue: Class<Component>) {
  // Add on Prototype_ init method
  Vue.prototype._init = function (options?: Object) {
    ...
    vm._self = vm
    initLifecycle(vm) // Initialize instance properties and data: $parent, $children, $refs, $root_ Watcher... Wait
    initEvents(vm) // Initialization events: $on, $off, $emit, $once
    initRender(vm) // Initialize rendering: render, mixin
    callHook(vm, 'beforeCreate') // Call lifecycle hook function
    initInjections(vm) // Initialize inject
    initState(vm) // Initialize component data: props, data, methods, watch, computed
    initProvide(vm) // Initialize provide
    callHook(vm, 'created') // Call lifecycle hook function
    ...
  }
}

Initialization here calls many methods. Each method does different things. The response type is mainly the data props and data in the component. The content of this block is in the initState() method, so let's go to the source code of this method and have a look

initState()

Source address: Src / core / instance / state.js - line 49

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // Initialize props
  if (opts.props) initProps(vm, opts.props)
  // Initialize methods
  if (opts.methods) initMethods(vm, opts.methods)
  // Initialize data 
  if (opts.data) {
    initData(vm)
  } else {
    // If there is no data, it will be assigned to an empty object by default and listen
    observe(vm._data = {}, true /* asRootData */)
  }
  // Initialize computed
  if (opts.computed) initComputed(vm, opts.computed)
  // Initialize watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

To call a bunch of initialization methods again, let's go straight to the topic and take the data related to our responsive data, that is, initProps(), initData(), observe()

One by one, you have to understand the whole process of response

initProps()

Source address: Src / core / instance / state.js - line 65

The main tasks here are:

  • Traverse the props list passed in by the parent component
  • Verify the name, type, default attribute, etc. of each attribute. If there is no problem, call defineReactive to set it to responsive
  • Then use proxy() to proxy the attributes to the current instance, such as VM_ If props.xx becomes vm.xx, you can access it
function initProps (vm: Component, propsOptions: Object) {
  // props passed from parent component to child component
  const propsData = vm.$options.propsData || {}
  // The final props after conversion
  const props = vm._props = {}
  // Store the props key. Even if the props value is empty, the key will still be in it
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // Convert props for non root instances
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    // Verify props type, default attribute, etc
    const value = validateProp(key, propsOptions, propsData, vm)
    // In a non production environment
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(`hyphenatedKey Is a reserved property and cannot be used as a component prop`)
      }
      // Set props to responsive
      defineReactive(props, key, value, () => {
        // Warn if the user modifies props
        if (!isRoot && !isUpdatingChildComponent) {
          warn(`Avoid direct changes prop`)
        }
      })
    } else {
      // Set props to responsive
      defineReactive(props, key, value)
    }
    // Proxy attributes that are not on the default vm to the instance
    // You can make VM_ Props.xx is accessed through vm.xx
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

initData()

Source address: Src / core / instance / state.js - line 113

The main tasks here are:

  • Initialize a data and get the keys collection
  • Traverse the keys collection to determine whether it has the same name as the property name in props or the method name in methods
  • If there is no problem, proxy every attribute in the data to the current instance through proxy(), and you can access it through this.xx
  • Finally, call observe to listen to the whole data
function initData (vm: Component) {
  // Get the data of the current instance 
  let data = vm.$options.data
  // Determine the type of data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(`The data function should return an object`)
  }
  // Gets the data property name collection of the current instance
  const keys = Object.keys(data)
  // Get props of current instance 
  const props = vm.$options.props
  // Gets the methods object of the current instance
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // Judge whether the methods in methods exist in props in non production environment
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(`Method Method cannot be declared repeatedly`)
      }
    }
    // Judge whether the attributes in data exist in props in non production environment
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(`Property cannot be declared repeatedly`)
    } else if (!isReserved(key)) {
      // Proxy to vm without duplicate names
      // You can make VM_ Data.xx is accessed through vm.xx
      proxy(vm, `_data`, key)
    }
  }
  // Monitor data
  observe(data, true /* asRootData */)
}

observe()

Source address: Src / core / observer / index.js - line 110

This method is mainly used to add a listener to the data

The main tasks here are:

  • If it is an object type of vnode or not a reference type, it will jump out directly
  • Otherwise, add an Observer to the data without adding an Observer, that is, a listener
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // If it is not an 'object' type or an object type of vnode, it will be returned directly
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // Using cached objects
  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
  ) {
    // Create listener
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

Source address: Src / core / observer / index.js - line 37

This is a class that converts normal data into observable data

The main tasks here are:

  • Mark the current value as a responsive attribute to avoid repeated operations
  • Then determine the data type
    • If it is an object, it traverses the object and calls defineReactive() to create a responsive object
    • If it is an array, traverse the array and call observe() to listen for each element
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // Number of VMS on the root object
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // Add to value__ ob__  Property, an Observe instance with value
    // The expression has become a response. The purpose is to skip directly when traversing the object to avoid repeated operations
    def(value, '__ob__', this)
    // Type judgment
    if (Array.isArray(value)) {
      // Determine whether the array has__ proty__
      if (hasProto) {
        // If so, override the array method
        protoAugment(value, arrayMethods)
      } else {
        // If not, define the property value through def, that is, Object.defineProperty
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // If object type
  walk (obj: Object) {
    const keys = Object.keys(obj)
    // Traverse all properties of the object and turn it into a responsive object. It is also a dynamic addition of getter s and setter s to achieve two-way binding
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  // Listener array
  observeArray (items: Array<any>) {
    // Traverse the array and listen for each element
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive()

Source address: Src / core / observer / index.js - line 135

The purpose of this method is to define a responsive object

The main tasks here are:

  • Initialize a dep instance first
  • If it is an object, call observe and listen recursively to ensure that it can become a responsive object no matter how deep the structure is nested
  • Then call Object.defineProperty() to hijack the getter and getter of object properties.
  • If the getter is triggered during acquisition, it will call dep.dependent () to push the observer into the dependent array subs, that is, dependency collection
  • If the setter is triggered during update, the following operations will be performed
    • The new value does not change or there is no direct jump out of the setter property
    • If the new value is an object, observe() is called to listen recursively
    • Then call dep.notify() to distribute updates.
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  // Create dep instance
  const dep = new Dep()
  // Get the property descriptor of the object
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // Get custom getter s and setter s
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // If val is an object, it will listen recursively
  // By calling observe recursively, you can ensure that no matter how deep the object structure is nested, it can become a responsive object
  let childOb = !shallow && observe(val)
  // Intercepting getter s and setter s of object properties
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // Intercept getter, and this function will be triggered when taking value
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // Perform dependency collection
      // When initializing the render watcher, access the object that needs two-way binding, which triggers the get function
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // Intercept setter, which will be triggered when the value changes
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // Determine whether there is a change
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // Accessor property without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // If the new value is an object, listen recursively
      childOb = !shallow && observe(newVal)
      // Distribute updates
      dep.notify()
    }
  })
}

As mentioned above, dep.depend is used for dependency collection. Dep is the core of the whole getter dependency collection

Dependency collection

Dep is the core of dependency collection, and it is also inseparable from Watcher. Let's take a look

Dep

Source address: src/core/observer/dep.js

This is a class, which is actually a kind of management of Watcher

Here, first initialize a sub array to store dependencies, that is, observers who depend on this data are in this array, and then define several methods to add, delete, notify and update dependencies

In addition, it has a static attribute target, which is a global Watcher, which also means that only one global Watcher can exist at the same time

let uid = 0
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  // Add observer
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  // Remove observer
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      // Call Watcher's addDep function
      Dep.target.addDep(this)
    }
  }
  // Distribute updates (described in the next chapter)
  notify () {
    ...
  }
}
// Only one observer is used at the same time. Assign an observer
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher

Source address: src/core/observer/watcher.js

Watcher is also a class, also called observer (subscriber). The work done here is quite complex, and it is also connected with rendering and compilation

Let's look at the source code first, and then go through the whole process of dependency collection

let uid = 0
export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // Array of Dep instances held by Watcher instance
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.value = this.lazy
      ? undefined
      : this.get()
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
  }
  get () 
    // This function is used to cache Watcher
    // Because when a component contains nested components, the Watcher of the parent component needs to be restored
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // Call the callback function, that is, upcateComponent, to evaluate the object that needs two-way binding, so as to trigger dependency collection
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
    } finally {
      // Depth monitoring
      if (this.deep) {
        traverse(value)
      }
      // Restore Watcher
      popTarget()
      // Cleaning up unwanted dependencies
      this.cleanupDeps()
    }
    return value
  }
  // Called on dependency collection
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // Push the current watcher into the array
        dep.addSub(this)
      }
    }
  }
  // Clean up unnecessary dependencies (below)
  cleanupDeps () {
    ...
  }
  // Called when distributing updates (below)
  update () {
    ...
  }
  // Execute callback of watcher
  run () {
    ...
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

Supplement:

  1. Why can we automatically get the new value and the old value of the watch written in our own component?
    The callback will be executed in watcher.run(), and the new and old values will be passed

  2. Why initialize two Dep instance arrays
    Because Vue is data-driven, every time the data changes, it will re render, that is, the vm.render() method will re execute and trigger the getter again. Therefore, it is represented by two arrays: the newly added Dep instance array newDeps and the last added instance array deps

Dependency collection process

There will be such logic when the first rendering is mounted

mountComponent source code address: Src / core / instance / lifecycle.js - line 141

export function mountComponent (...): Component {
  // Call lifecycle hook function
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
    // Call_ update patch es (that is, Diff) the virtual DOM returned by render to the real dom. This is the first rendering
    vm._update(vm._render(), hydrating)
  }
  // Set an observer for the current component instance and monitor the data obtained by the updateComponent function, as described below
  new Watcher(vm, updateComponent, noop, {
    // When triggering updates, it will be called before updating.
    before () {
      // Judge whether the DOM is in the mounted state, that is, it will not be executed when rendering and uninstalling for the first time
      if (vm._isMounted && !vm._isDestroyed) {
        // Call lifecycle hook function
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // There is no old vnode, indicating that it is the first rendering
  if (vm.$vnode == null) {
    vm._isMounted = true
    // Call lifecycle hook function
    callHook(vm, 'mounted')
  }
  return vm
}

Dependency collection:

  • Before mounting, a render Watcher will be instantiated. After entering the watcher constructor, this.get() method will be executed
  • Then it will execute pushTarget(this), that is, assign Dep.target to the current render watcher and push it onto the stack (for recovery)
  • Then execute this.getter.call(vm, vm), that is, the updateComponent() function above, where VM. _update (VM. _render()) is executed
  • Then execute vm._render() to generate a rendered vnode. In this process, the data on the vm will be accessed, and the getter of the data object will be triggered
  • Every getter of object value has a dep. when the getter is triggered, the dep.depend() method will be called and Dep.target.addDep(this) will be executed
  • Then, some judgments will be made here to ensure that the same data will not be added multiple times. Then, push the qualified data into the subs. At this point, the collection of dependencies has been completed, but it has not been completed yet. If it is an object, it will recurse the object to trigger getter s of all children, and restore the Dep.target state

Remove subscription

To remove a subscription is to call the cleanupDeps() method. For example, in the template, v-if we have collected the dependencies in the qualified template A. when the conditions change, template b is displayed and template a is hidden. In this case, we need to remove the dependencies of A

The main tasks here are:

  • First, traverse the last added instance array deps, and remove the Watcher subscription in the dep.subs array
  • Then exchange newDepIds and depIds, and exchange newDeps and deps
  • Then clear newDepIds and newDeps
// Cleaning up unwanted dependencies
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

Distribute updates

notify()

When the setter is triggered, dep.notify() will be called to notify all subscribers to send updates

notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // If it is not asynchronous, sorting is required to ensure that it is triggered correctly
      subs.sort((a, b) => a.id - b.id)
    }
    // Traverse all watcher instance arrays
    for (let i = 0, l = subs.length; i < l; i++) {
      // Trigger update
      subs[i].update()
    }
  }

update()

Called when an update is triggered

  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // Component data update will go here
      queueWatcher(this)
    }
  }

queueWatcher()

Source address: Src / core / observer / scheduler.js - line 164

This is a queue and an optimization point for Vue when distributing updates. That is, the watcher callback will not be triggered every time the data changes, but these watchers will be added to a queue and executed after nextTick

The logic here overlaps with that of flushscheduler queue () in the next section, so it should be understood together

The main tasks are:

  • First use the has object to find the id to ensure that the same watcher will only push once
  • else, if a new watcher is inserted during the execution of the watcher, it will come here. Then, look back and forward, find the position where the first id to be inserted is larger than the id in the current queue, and insert it into the queue. In this way, the length of the queue will change
  • Finally, ensure that nextTick will only be called once through waiting
export function queueWatcher (watcher: Watcher) {
  // Get the id of the watcher
  const id = watcher.id
  // Judge whether the watcher of the current id has been push ed
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // I'll be here at first
      queue.push(watcher)
    } else {
      // When executing the following flushscheduler queue, if there is a newly distributed update, it will enter here and insert a new watcher, as described below
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // I'll be here at first
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      // Because each update will cause rendering, all watcher s are called in nextTick
      nextTick(flushSchedulerQueue)
    }
  }
}

flushSchedulerQueue()

Source address: Src / core / observer / scheduler.js - line 71

The main tasks here are:

  • First sort the queue. There are three sorting conditions. See the notes
  • Then traverse the queue and execute the corresponding watcher.run(). It should be noted that the queue length will be evaluated every time during traversal, because after run, a new watcher may be added, and the above queueWatcher will be executed again
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort by id, with the following conditions
  // 1. Component updates need to be in the order from parent to child, because the creation process also starts from parent to child
  // 2. The watcher written by ourselves in the component takes precedence over the render watcher
  // 3. If a component is destroyed during the operation of the watcher of the parent component, skip the watcher
  queue.sort((a, b) => a.id - b.id)

  // Do not cache the queue length, because the queue length may change during traversal
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // Execute beforeUpdate lifecycle hook function
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // Execute the callback function of watch written by ourselves in the component and render the component
    watcher.run()
    // Check and stop the cyclic update. For example, if the object is re assigned during the watcher process, it will enter the infinite loop
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(`Infinite cycle`)
        break
      }
    }
  }
  // Keep a backup of the queue before resetting the status
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  resetSchedulerState()
  // Call the hook activated by the component
  callActivatedHooks(activatedQueue)
  // Call the hook updated for component update
  callUpdatedHooks(updatedQueue)
}

updated()

Finally, it can be updated. Updated is familiar to everyone. It is the life cycle hook function

When callUpdatedHooks() is called above, it will enter here and execute updated

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

So far, the source code of Vue2's responsive principle process has been basically analyzed. Next, let's introduce the shortcomings of the above process

defineProperty defects and handling

There are still some problems with using Object.defineProperty to implement responsive objects

  • For example, when adding a new property to an object, the setter cannot be triggered
  • For example, changes in array elements cannot be detected

Vue2 also has corresponding solutions to these problems

Vue.set()

When adding new responsive properties to an object, you can use a global API, the Vue.set() method

Source address: Src / core / observer / index.js - line 201

The set method receives three parameters:

  • target: array or ordinary object
  • Key: indicates the array subscript or the key name of the object
  • val: indicates the new value to replace

The main tasks here are:

  • First, if it is an array and the subscript is legal, replace it directly with the rewritten splice
  • If it is an object and the key exists in the target, replace the value
  • If not__ ob__, Description is not a responsive object. It is returned by direct assignment
  • Finally, the new attribute is changed into a response type and the update is distributed
export 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 it is an array and is a legal subscript
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    // Use the splice directly to replace it. Note that the splice here is not native, so it can be monitored. See the following for details
    target.splice(key, 1, val)
    return val
  }
  // This indicates that it is an object
  // If the key exists in the target, it can be directly assigned and monitored
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // Get target__ ob__
  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
  }
  // As described in Observer, if there is no such attribute, it means that it is not a responsive object
  if (!ob) {
    target[key] = val
    return val
  }
  // Then change the newly added attribute into a response
  defineReactive(ob.value, key, val)
  // Manually distribute updates
  ob.dep.notify()
  return val
}

Override array method

Source address: src/core/observer/array.js

The main tasks here are:

  • Save a list of methods that will change the array
  • When executing some methods in the list, such as push, first save the original push, then perform responsive processing, and then execute this method
// Gets the prototype of the array
const arrayProto = Array.prototype
// Create an object that inherits the array prototype
export const arrayMethods = Object.create(arrayProto)
// Will change the method list of the original array
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
// Override array events
methodsToPatch.forEach(function (method) {
  // Save original event
  const original = arrayProto[method]
  // Create a responsive object
  def(arrayMethods, method, function mutator (...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)
    // Distribute updates
    ob.dep.notify()
    // After finishing the processing we need, we can execute the original event
    return result
  })
})

Previous highlights

epilogue

If this article is of little help to you, please give me a praise and support. Thank you

Posted by Yegg on Sat, 09 Oct 2021 23:16:44 -0700