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:
- Why array methods need to be rewritten
- How to rewrite these methods
- How to optimize dependency triggering during the rewriting process
- 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:
- First access the
arr.pushproperty (to get the push function) - 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
- Necessity: Proxy cannot directly intercept calls to array methods, so they must be rewritten.
- Optimization Strategy: Pause dependency collection + batch trigger updates to avoid unnecessary re-execution.
- Precise Triggering: Trigger different updates based on different array operation types (ADD/SET/DELETE).
- Maintain Original Behavior: Rewritten methods must maintain exactly the same return values and behavior as the original array methods.
- Performance Consideration: Avoid collecting dependencies on intermediate states during array method execution via
pauseTrackingandresetTracking.
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.