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
- A bug where object.definedProperty cannot listen for new attributes is replaced by a manual call to set
- 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
- 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
- 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
- A special identity is used as the key for dependency collection when traversing groups or objects
- 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
- Intercept the native API and do some enhancements
- The ‘includes’, ‘indexOf’, and ‘lastIndexOf’ methods are used to collect dependencies on each item of the array
- 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