To trigger the update, we need to do dependency collection before that happens. But how do you distribute updates to new attributes that don’t rely on collection? Notify updates with previously collected dependencies.

New vue2 attributes are updated

Update the new property of the object

Because vue2 uses Object.definedProperty to implement the reactive principle, there is no native support for intercepting new attributes. Vue2 provides a set method that lets you listen for new attributes and trigger updates.

function set (target: Array<any> | Object, key: any, val: any) :any {... defineReactive(ob.value, key, val)// Internally re-listen for new attributes using Object.definedProperty
  ob.dep.notify()                      // Trigger the update of the target object of the new attribute to replace the defect that the new attribute cannot be updated
  return val
}

Copy the code

The core idea

  1. A bug where object.definedProperty cannot listen for new attributes is replaced by a manual call to set
  2. Because there are no collection dependencies for the new attribute, updates are distributed with the object of the new attribute

Array calls updates to common apis

Calling array apis such as push and splice in VUe2 will trigger the update, so we need to intercept the native API and add the operation of distributing the update. If we modify the array. prototype prototype method directly, we will pollute the Array prototype and the normal Array API will be modified. The best of all is to create a new object that inherits the array stereotype, and extending on the new object requires an enhancement method that triggers the update.

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto) // Create a new object that inherits the array stereotype

const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

methodsToPatch.forEach(function (method) {  
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (. args) {  // Extend a new method of the same name on the arrayMethods object
    const result = original.apply(this, args)  // Call the array native API
    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()     // Triggers an update of array collection dependencies
    return result
  })
})
Copy the code

But is this the end of it? How do we map to an arrayMethods object when we call an array method? So we need to intercept an array when we access it and delegate it to an arrayMethods object.

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 (Array.isArray(value)) {
      if ('__proto__' in {}) {
        protoAugment(value, arrayMethods)   // Point the array prototype to arrayMethods
      } else {
        copyAugment(value, arrayMethods, arrayKeys)   // Extend the methods in arrayMethods to arrays
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  ...
}
Copy the code

The core idea

  1. Intercepting the original array API, generating a new method (calling the original method, dispensing updates) that extends to the new object inherited from the array prototype
  2. The array is accessed by proxy on the newly created object

Updates to vue3 new attributes

Because vue3’s reactive API uses a proxy, which is an object, it naturally supports listening for new attributes of the object. As with vue2, there is no dependency collection for new attributes. How can updates be triggered?

Update of new properties

Let’s start with an example

const obj = {a:1}
const proxyObj = reactive(obj)
effect(() = >{
  console.log("... update".JSON.stringify(proxyObj));
})
proxyObj.b = 2
Copy the code

Does that trigger an update? Yes. Json.stringify traverses each property of the object, so the value of the new property is different from the value of the new property. Vue3 uses a special identifier to collect dependencies while traversing, and then triggers the update of the collected dependencies for that particular identifier after the new property is added.

new Proxy(target,{
  get,
  set,
  deleteProperty,
  has,
  ownKeys: function ownKeys(target: object) : (string | number | symbol) []{
    track(target, "iterate", isArray(target) ? 'length' : Symbol("iterate")) // Iterate groups use length as a special identifier to collect dependencies, objects use Symbol("iterate")
    return Reflect.ownKeys(target)
  }
})

Copy the code

Traversal groups and objects collect dependencies with different identifiers and distribute updates when new attributes are added.

new Proxy(target,{
  get,
  set:function set(target: object, key: string | symbol, value: unknown, receiver: object) :boolean {
    const oldValue = (target as any)[key]
    ...
    // Check whether the index of an array is less than array length, and hasOwnProperty checks whether the object is an attribute.
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key) 
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if(! hadKey) { trigger(target,"add", key, value)    // Update the new attribute
      } else if (hasChanged(value, oldValue)) {
        trigger(target, "set", key, value, oldValue)
      }
    }
    return result
  },
  deleteProperty,
  has,
  ownKeys
})
Copy the code

How does the trigger function handle updates to new attributes

function trigger(target: object, type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
  // targetMap is a weakMap, store target corresponds to a map mapping that stores all attributes,map is the mapping of each attribute object effect set targetMap
      
        map
       
         set
        
       ,>
      ,map>
  const depsMap = targetMap.get(target)
  if(! depsMap) {// never been tracked
    return
  }
    
  // Set of temporary effects to execute
  const effects = new Set<ReactiveEffect>()
  // Add effetct to the effects collection
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }// If target is an array and key changes length, for example, if [1,2,3] changes length=1, the array changes to [1]
  if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) = > {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if(key ! = =void 0) {
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case "add": // Add attributes
        if(! isArray(target)) { add(depsMap.get(Symbol("itrator")))   // Add a traversal Symbol("itrator") to the object to indicate the effect of the collection
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))    // The effect of the collection is added to the array with a traversal length
        }
        break
      case "delete":
        if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case "set":
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break}}// Execute each effect
  const run = (effect: ReactiveEffect) = > {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  // Iterate over the Effects collection
  effects.forEach(run)
}
Copy the code

The core idea

  1. A special identity is used as the key for dependency collection when traversing groups or objects
  2. The dependency on this particular identity collection is triggered when a new attribute is added

Call the array API update

Take a look at the interception of array methods in proxy

new Proxy(target,{
  get: function get(target: Target, key: string | symbol, receiver: object) {...const targetIsArray = isArray(target)
    The proxy calls the arrayInstrumentations method if it is an array and key is an arrayInstrumentations attribute
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    ...
  },
  set,
  deleteProperty,
  has,
  ownKeys
})
Copy the code

Interception of ‘includes’, ‘indexOf’, and ‘lastIndexOf’ methods. These methods traverse the entire array to find an element, so any change in the value of any item in the array may affect the result of the execution, so each item needs to be collected and updated as it changes

(['includes'.'indexOf'.'lastIndexOf'] as const).forEach(key= > {
  const method = Array.prototype[key] as any
  arrayInstrumentations[key] = function(this: unknown[], ... args: unknown[]) {
    const arr = toRaw(this)
    // Collect dependencies on each item in the array
    for (let i = 0, l = this.length; i < l; i++) {
      track(arr, "get", i + ' ')}// Implement native methods
    const res = method.apply(arr, args)
    if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      return method.apply(arr, args.map(toRaw))
    } else {
      return res
    }
  }
})
Copy the code

‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’ because when executing these methods, some other properties are accessed, such as: length, etc. So you need to pause the dependency collection while these methods are executed and resume the collection when they are done.

(['push'.'pop'.'shift'.'unshift'.'splice'] as const).forEach(key= > {
  const method = Array.prototype[key] as any
  arrayInstrumentations[key] = function(this: unknown[], ... args: unknown[]) {
    pauseTracking()  // Pause the collection
    const res = method.apply(this, args)
    resetTracking()  // Resume collection
    return res
  }
})

Copy the code

The core idea

  1. Intercept the native API and do some enhancements
  2. The ‘includes’, ‘indexOf’, and ‘lastIndexOf’ methods are used to collect dependencies on each item of the array
  3. Methods ‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’ pause dependency collection and resume after execution

conclusion

1. No matter vue2 or VUe3, new attributes are distributed with the help of previously collected dependent attributes. Vue2 relies on the dependencies that are collected when the object of the new attribute is accessed. Vue3 relies on the special values defined in the ownKeys interception method after the proxy proxy.

2. Intercepting the native array API for enhancement, with the ability to rely on the collection or distribution of updates