Source Code Level Rewriting of 7 Array Mutation Methods and Dependency Trigger Optimization in Vue3's Reactivity System

Source Code Level Rewriting of 7 Array Mutation Methods and Dependency Trigger Optimization in Vue3's Reactivity System

Description

In Vue3's reactivity system, arrays are a special type of object. To ensure that mutation operations on arrays (such as push, pop, splice, etc.) can trigger reactive updates, Vue3 rewrites 7 array mutation methods. This knowledge point mainly explores:

  1. Why array methods need to be rewritten
  2. How to rewrite these methods
  3. How to optimize dependency triggering during the rewriting process
  4. The difference from reactive handling of ordinary objects

Problem-Solving Process

Step 1: Understanding the Particularity of Array Reactivity

Arrays in JavaScript are special objects, and their indices are essentially properties. However, array mutation methods (push, pop, shift, unshift, splice, sort, reverse) modify the array itself, and ordinary property setting cannot effectively track these changes.

Problem: When creating a reactive array via reactive():

const arr = reactive([1, 2, 3])
arr.push(4) // If the push method is not rewritten, this operation will not trigger dependency updates

Reason: Proxy can only intercept property access and setting operations, while arr.push(4) actually is:

  1. First access the arr.push property (to get the push function)
  2. Then call this function
    Proxy cannot know how the function modifies the array internally.

Step 2: Locating the Array Method Rewriting in the Source Code

In the Vue3 source code, array method rewriting is mainly implemented in packages/reactivity/src/baseHandlers.ts and packages/reactivity/src/reactive.ts.

Key Code Structure:

// 1. Define the 7 mutation methods
const arrayInstrumentations: Record<string, Function> = {}

// 2. Rewrite these methods
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach(key => {
  arrayInstrumentations[key] = function (this: unknown[], ...args: unknown[]) {
    // Pause dependency collection to avoid unnecessary triggering
    pauseTracking()
    
    // Call the original method
    const res = (toRaw(this) as any)[key].apply(this, args)
    
    // Resume dependency collection
    resetTracking()
    return res
  }
})

Step 3: Understanding the Core Logic of the Rewritten Methods

Each mutation method is rewritten following a similar pattern:

Take the push method as an example:

arrayInstrumentations.push = function (this: unknown[], ...args: unknown[]) {
  // Step 3.1: Pause dependency collection
  // This is to avoid triggering unnecessary dependencies when getting the array length and setting new elements
  pauseTracking()
  
  // Step 3.2: Execute the original push method
  const res = (toRaw(this) as any).push.apply(this, args)
  
  // Step 3.3: Resume dependency collection
  resetTracking()
  
  // Step 3.4: Manually trigger dependency updates
  // Two types of dependencies need to be triggered here:
  // 1. Dependencies on the array's length property
  // 2. Dependencies on the indices of newly added elements
  trigger(this, TriggerOpTypes.ADD, '')
  
  return res
}

Step 4: Optimization Strategy for Dependency Triggering

Vue3 implements the following optimizations when rewriting array methods:

Optimization Point 1: Pause Dependency Collection

function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}
  • Pause all dependency collection when executing array mutation methods.
  • Reason: Array methods may internally access the array's length property and other properties. If dependencies are collected at this time, it would cause unnecessary re-execution.

Optimization Point 2: Batch Trigger Updates
Consider the following scenario:

arr.push(1, 2, 3) // Add multiple elements at once
  • If updates were triggered each time an element is added, there would be 3 triggers.
  • After optimization: Updates are triggered in one batch after the method execution completes.

Optimization Point 3: Precise Trigger Types
Based on different array methods, different update types are triggered:

// ADD type: push, unshift, splice(adding)
trigger(this, TriggerOpTypes.ADD, '')

// SET type: splice(replacement)
trigger(this, TriggerOpTypes.SET, index.toString())

// DELETE type: pop, shift, splice(deletion)
trigger(this, TriggerOpTypes.DELETE, '')

Step 5: Viewing the Complete Rewriting Logic

Take the splice method as an example (the most complex array method):

arrayInstrumentations.splice = function (
  this: unknown[],
  start: number,
  deleteCount?: number,
  ...items: unknown[]
) {
  const raw = toRaw(this)
  
  // Pause dependency collection
  pauseTracking()
  
  // Execute the original splice
  const res = raw.splice.apply(raw, [start, deleteCount, ...items])
  
  // Resume dependency collection
  resetTracking()
  
  // Trigger updates based on operation type
  if (items.length > 0 || deleteCount! > 0) {
    // If new elements are added, trigger ADD
    if (items.length > 0) {
      trigger(this, TriggerOpTypes.ADD, '', items)
    }
    // If elements are deleted, trigger DELETE
    if (deleteCount! > 0) {
      trigger(this, TriggerOpTypes.DELETE, '', undefined)
    }
  }
  
  return res
}

Step 6: Understanding the Connection to the Original Array

The rewritten methods need to call the original array's methods:

// Get the original array's method
const originMethod = Array.prototype.push

// Call the original method within the rewritten method
arrayInstrumentations.push = function (...args) {
  // Get the original array via toRaw
  const raw = toRaw(this)
  
  // Call the original array's push method
  const res = originMethod.apply(raw, args)
  
  // ... Trigger updates and other operations
  return res
}

Step 7: Understanding Integration into the Proxy Handler

When creating a Proxy for an array, the rewritten methods need to be returned in the get trap:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // ... Other code
    
    // If it's an array and a mutation method is being accessed
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // Return the rewritten method
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    
    // ... Other cases
  }
}

Key Point Summary

  1. Necessity: Proxy cannot directly intercept calls to array methods, so they must be rewritten.
  2. Optimization Strategy: Pause dependency collection + batch trigger updates to avoid unnecessary re-execution.
  3. Precise Triggering: Trigger different updates based on different array operation types (ADD/SET/DELETE).
  4. Maintain Original Behavior: Rewritten methods must maintain exactly the same return values and behavior as the original array methods.
  5. Performance Consideration: Avoid collecting dependencies on intermediate states during array method execution via pauseTracking and resetTracking.

Practical Application Example

const arr = reactive([1, 2, 3])

// When push is called, the rewritten method is actually invoked
arr.push(4)

// This will be intercepted by the Proxy, returning the rewritten push method
// The rewritten push method will:
// 1. Pause dependency collection
// 2. Call the original push
// 3. Resume dependency collection
// 4. Trigger updates

Through this design, Vue3 ensures that array mutation operations can correctly trigger reactive updates while optimizing performance by avoiding unnecessary recomputations.