SoFunction
Updated on 2025-03-10

Detailed explanation of the special handling of responsive formulas in Vue3

vue2 vs vue3

The core difference between the two responsive updates isandProxyTwo API problems, through these two APIs, the main responsive problems can be solved. For some cases, special treatment is required

Responsiveness that cannot be implemented in vue2

  • arr[0] = newVal
  • obj[newKey] = value
  • delete

For these cases, vue2 is added byVue.$setand rewrite array methods to implement. However, for vue3, becauseproxyIt is the proxy for the entire object, so it is born to support oneFeatures that cannot be supported, such as it can listen to add new attributes, andBecause the agent is everykeySo it doesn't know the new attributes. Such, the following lists some different responsive processing in vue3.

New attribute updates

Although proxy can listen to the addition of new attributes, the newly added attributes have not been collected through getters like existing attributes, which means that updates cannot be triggered. So our purpose is how to collect responses

First, let's take a look at how to deal with it in vue3for...in..Looping, it can be known that the loop is used internally(obj) to obtain keys that belong only to the object itself. So forfor..in The interception of the loop is clear

const obj = {foo: 1}
const ITERATE_KEY = symbol()
const p = new Proxy(obj, {
   track(target, ITERATE_KEY)
   return (target)
})

Here, we used onesymboldata collected as a dependencykeyBecause this is our interception operation in traversal is not associated with the specific key, but is an integral interception. When triggering a response, just trigger thissymbolCollectedeffectThat's fine

trigger(target, isArray(target) ? 'length' : ITERATE_KEY) // Array situation tracking length

This will affect the length of the traversal object and will be introduced.ITERATE_KEY Related side effect function execution

effect(() => {
    for(let i in obj) {
        (i)
    }
})

After the side effect function is executed, we execute the rendering function similarly. Then go back to our new attributes,

 = 1

Because the new attributes will be correctfor.. in ..The cycle has an impact, so we need toITERATE_KEY Take out the related side effect functions and re-execute them to see how this part is processed in the source code.

First of all, here issetterHandling

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    ...
    // Here is a sign of whether there is a key, that is, to determine whether it is a new element    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < 
        : hasOwn(target, key)
    const result = (target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
         // This shows that when new elements are added, trigger logic is taken        trigger(target, , key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, , key, value, oldValue)
      }
    }
    return result
  }
}

Then there is the specifictrigger, get the corresponding logo to updateeffect

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = (target)
  if (!depsMap) {
    // never been tracked
    return
  }
    ...

    // also run for iteration key on ADD | DELETE | 
    switch (type) {
    // This situation is the execution of trigger we just determined      case :
        if (!isArray(target)) {
        // Get the ITERATE_KEY dependency collected          ((ITERATE_KEY))
          if (isMap(target)) {
            ((MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          (('length'))
        }
        break
         ...
  }
    for (const dep of deps) {
      if (dep) {
        (...dep)
      }
    }
    ...
    triggerEffects(createDep(effects))
    ...
  }
}

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || ) {
    if (__DEV__ && ) {
      (extend({ effect }, debuggerEventExtraInfo))
    }
    if () {
      ()
    } else {
     // Execute all effect functions      ()
    }
  }
}

Summarize:

When traversing an object or array, use a unique identifiersymbolCollect dependencies

  • In fact, if you use obj directly in a template, it will be accompanied by a process and will also be accompanied by a collection of dependencies.
  • If we do not use traversal objects in the js code, adding a single object will not trigger an update because there is no collection process.

Get the collected value when setting a new valuesymbolCorresponding side effect function update

Processing of traversal array methods

When using arrays, it will be accompanied bythisThe problem of the proxy object cannot get the attributes, such as

const obj = {}
const arr = reactive([obj])
((obj) // false

This is becauseincludesInternalthisIt points to the proxy object arr, and when comparing the elements, it is also the proxy object, so it is definitely not found by looking for the original object. Therefore, we need to modify the behavior of inlcudes.

new Proxy(obj, {
    get() {
        if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
          return (arrayInstrumentations, key, receiver)
        }
    }
})
   
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

// Handle problems in centralized array traversal methods.function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  // instrument identity-sensitive Array methods to account for possible reactive
  // values 
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      const arr = toRaw(this) as any
      for (let i = 0, l = ; i < l; i++) {
        track(arr, , i + '')
      }
      // we run the method using the original args first (which may be reactive)
      // Use the original parameters first, which may be the original object or the proxy object.      const res = arr[key](...args)
      if (res === -1 || res === false) {
        // if that didn't work, run it again using raw values.
        // If not found, take the original parameters to compare and remove the responsive data.        return arr[key](...(toRaw)) 
      } else {
        return res
      }
    }
  })

How to change arrays

For array methods that may change the length of the original array,push, pop, shift, unshift, spliceIt also needs to be processed, otherwise, it will fall into infinite recursion, considering the following scenario

cosnt arr = reactive([])
effect(() => {
    (1)
})
effect(() => {
    (1)
})

The execution process of these two is as follows:

  • First, the first side effect function is executed, and then 1 is added to the array, and this process will affect the arraylength, so it will be withlengthWill betrack, establish responsive contacts.
  • Then the second side effect function is executed,pushThis is because of the impactlength,FirsttrackEstablish a responsive connection, and then try to take out the side effect function of length, which is the first side effect function to execute. However, before the second side effect function is completed, the first side effect function will be executed again.
  • The first side effect function is executed again and will also readlengthAnd setlength, repeat the above collection and update process, and then execute the length collected from the second side effect
  • This cycle goes on and on. Eventually, the stack will overflow.

So the key to the problem lies inlengthContinuous reading and setting. So we need to read length to avoid making a connection between it and side effect functions

;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // Tracking changes is prohibited.      pauseTracking()
      const res = (toRaw(this) as any)[key].apply(this, args)
      //Reply to track when the function is executed.      resetTracking()
      return res
    }
  })

Summarize

vue2andvue3For newly added attributes, you need to obtain previously collected dependencies before you can distribute updates.vue2Used inVue.$setRelying on the existing dependencies of objects with newly added attributes, the update is distributed.vue3It's becauseownKeys()The collected before interceptsymbolDependency, triggers this when adding attributessymbol Collected dependency updates.

For arrays,vue2It is a method to intercept and modify the array, and then update the dependencies collected by the current array.()vue3Because it has a certain ability to update array elements, but because it is becauselengthThe stack overflow caused by this is therefore prohibited to track. At the same time, the access method also needs to be updated because the problem of inconsistency between the proxy object and the original object is solved by comparing the two when searching.

The above is a detailed explanation of the special processing of responsiveness in Vue3. For more information about responsiveness in Vue3, please pay attention to my other related articles!