Before we begin, let’s briefly explain how reactive Proxies work.
The principle is briefly
Proxy is a new object in ES6. You can perform a layer Proxy for a specified object.
The objects we create through Proxy go through no more than two steps to meet our responsive requirements.
- collect
watch
Dependencies in functions. - Change and call again
watch
Function.
How to collect dependencies
After the object is Proxy, we can add a layer of interception to read the object’s properties, usually in the form of:
const p = new Proxy({}, {
get(target, key, receiver) {
/ / intercept}});Copy the code
In the get interception method, we can get the object itself target, the read property key, and the caller receiver (the P object), where we can get the currently accessed property key.
Normally we access the proxied object in a method:
function fn(){
console.log(p.value);
}
fn();
Copy the code
When we execute the fn function, we will trigger our GET interception. We only need to record the function currently executed in the GET interception to establish a mapping of key => fn, and we can call the FN function again after the property value changes.
So the puzzle is how to perform our GET interception and still get which function called the proxy object.
In the Vue3 implementation, we use an effect function to wrap our own function.
effect(() = >{
console.log(p.value)
})
Copy the code
To save the function that called the proxy object.
let activeEffect;
function effect(fn){
activeEffect = fn;
fn(); / / execution
activeEffect = null;
}
// ...
get(target, key, receiver) {
// Get intercepts access the global activeEffect, which is the currently invoked function
// key => activeEffect
}
Copy the code
Another important point to note in get interception is that if the object we need to broker is an array, we will actually trigger the GET interception when we call most array methods such as push, POP, includes, etc., all of which access the length property of the array.
Trigger the Watch function
We will trigger the function for the saved key => fn map after the value changes. Set interception is triggered when a property value is set.
const p = new Proxy({}, {
set(target, key, value, receiver) {
// Retrieve the fn corresponding to the key to execute}});Copy the code
Other interception methods
In addition to get interception when we read properties, we need to collect dependencies in other operations to improve responsiveness.
has
.in
Operator interception.ownKeys
- intercept
Object.getOwnPropertyNames()
. - intercept
Object.getOwnPropertySymbols()
. - intercept
Object.keys()
. - intercept
Reflect.ownKeys()
.
- intercept
Remove the set interception that sets the property to fire the dependent function, and also need to fire when the property is deleted.
deleteProperty
Intercepts when deleting properties.
In addition to proxies for ordinary objects and arrays, there is another difficulty in proxies for Map and Set objects.
The detailed implementation of the principle can be found in my previous links, but is not implemented in this article.
- How to implement a responsive object using Proxy
- How to use Proxy to intercept Map and Set operations
Next, enter the body part.
Source analyses
Vue3 is Monorepo, and the reactive package ReacitVity is a separate package.
Reactivity is inspired by these three packages and, as I happen to have read the observer-util source code, there are many clever improvements and enhancements to ReActivity compared to its predecessor.
- increased
shallow
Mode, only the first value is responsive. - increased
readonly
Mode, does not collect dependencies, cannot be modified. - increased
ref
Object.
File structure
├ ─ ─ baseHandlers. Ts ├ ─ ─ collectionHandlers. Ts ├ ─ ─ computed. The ts ├ ─ ─ effect. The ts ├ ─ ─ but ts ├ ─ ─ operations. The ts ├ ─ ─ Reactive. Ts └ ─ ─ ref. TsCopy the code
BaseHandlers and collectionHandlers are the main implementation files of the function, which is the interceptor function corresponding to the Proxy object, and Effect is the observer function file.
This paper mainly analyzes these three parts.
Object data structure
The Target type is the original object that needs a Proxy, which defines four internal properties.
export interfaceTarget { [ReactiveFlags.SKIP]? :boolean[ReactiveFlags.IS_REACTIVE]? :boolean[ReactiveFlags.IS_READONLY]? :boolean[ReactiveFlags.RAW]? :any
}
Copy the code
TargetMap is a WeakMap of the dependency functions that are internally saved.
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
Copy the code
Its key name is the raw object without Proxy responsiveness, and its value is Map of key => Set< dependent function >.
We get the Map of the current object’s key => Set< dependent functions > via targetMap, extract all dependent functions from the key, and then call them when the value changes.
The following four maps are mappings between the original Target object and reactive or Readonly object.
export const reactiveMap = new WeakMap<Target, any> ()export const shallowReactiveMap = new WeakMap<Target, any> ()export const readonlyMap = new WeakMap<Target, any> ()export const shallowReadonlyMap = new WeakMap<Target, any> ()Copy the code
baseHandlers
The baseHandlers file basically creates a Proxy interceptor function for ordinary objects, arrays.
Start with the collection-dependent GET interceptor.
get
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// The Target internal key name is not stored on the object. Instead, get intercepts the return of the closure
if (key === ReactiveFlags.IS_REACTIVE) {
return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
// Target is the raw value, provided that the receiver is the same as the object in the raw => proxy
return target
}
const targetIsArray = isArray(target)
// Special handling for arrays
if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
// Ignore the built-in symbol and non-trackable keys
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// readonly cannot be changed without tracing
if(! isReadonly) {// Collect dependencies
track(target, TrackOpTypes.GET, key)
}
Shallow returns the result directly and does not respond to the nested object
if (shallow) {
return res
}
// ref processing
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
}
// If the value is an object, delay converting the object
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
Copy the code
In the GET interceptor, the first is a clever manipulation that returns the value of ReactiveFlags without actually assigning its value to the object, followed by special manipulation of arrays. The collection depends on the function track, defined in effect.ts, which we’ll look at later. If the value returned is an object, converting the object is delayed.
set
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
) :boolean {
let oldValue = (target as any)[key]
if(! shallow) { value = toRaw(value) oldValue = toRaw(oldValue)// if the old value is ref, there is also set interception inside ref,
if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true}}else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
// The array determines whether the index exists, and the object determines whether the key exists
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) {/ / there is no key to ADD
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
/ / a key SET
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
Copy the code
The set interceptor mainly determines whether the set key exists, and then divides two parameters to trigger. Trigger function is the method to trigger and collect effect function, which is also defined in effect.ts, so it is not mentioned here.
ownKeys
function ownKeys(target: object) : (string | symbol) []{
// For arrays, key is length; for objects, ITERATE_KEY is used only as a key identifier
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
Copy the code
The ownKeys interceptor is also a collection dependency. Note that the key argument passed in is length (target) and ITERATE_KEY (object). ITERATE_KEY is only a symbol identifier. In the future, the corresponding effect function will be obtained by this value. In fact, there is no such key.
effect
As mentioned in the principle statement of this article, if we want to know which function called the object, we need to put the function into our own running function to call. In the actual code we wrap the function passed into the effect method with a new type of ReactiveEffect.
The data structure
export interface ReactiveEffect<T = any> {
(): T
_isEffect: true
id: number
active: boolean // Whether it is valid
raw: () = > T // The original function
deps: Array<Dep> // The Set that holds effect depending on the key
options: ReactiveEffectOptions
allowRecurse: boolean
}
Copy the code
The most important field is deps. If we collect dependencies by executing this effectFn function, we get the following dependency structure:
{
"key1": [effectFn] // Set
"key2": [effectFn] // Set
}
Copy the code
So the deps property of our ReactiveEffect method effectFn is the Set corresponding to these two keys.
export function effect<T = any> (
fn: () => T, // The function passed in
options: ReactiveEffectOptions = EMPTY_OBJ
) :ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options) / / create ReactiveEffect
if(! options.lazy) { effect()/ / ReactiveEffect execution
}
return effect
}
Copy the code
A ReactiveEffect is created with createReactiveEffect in the effect function.
function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
const effect = function reactiveEffect() :unknown {
if(! effect.active) {return fn()
}
if(! effectStack.includes(effect)) { cleanup(effect)try {
enableTracking()
effectStack.push(effect)
// Assign activeEffect to current effect
activeEffect = effect
// Execute the function. The corresponding interceptor can save the corresponding effect via activeEffect
return fn()
} finally {
effectStack.pop()
resetTracking()
/ / reset activeEffect
activeEffect = effectStack[effectStack.length - 1]}}}asReactiveEffect effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect =true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
Copy the code
Before executing the effect function, save the function to the global variable activeEffect so that the interceptor can know which function is currently executing when collecting dependencies.
cleanup
The cleanup method cleans up dependencies.
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0}}Copy the code
The structure of the dePS attribute is described above. It saves sets that depend on effectFn, iterates through them, and removes effectFn from all sets.
track
The track method collects dependencies and is very simple to add ActiveEffects to the Dep.
export function track(target: object.type: TrackOpTypes, key: unknown) {
if(! shouldTrack || activeEffect ===undefined) {
return
}
let depsMap = targetMap.get(target)
// Target => Map
,dep>
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key)
// Set key
if(! dep) { depsMap.set(key, (dep =new Set()))}ActiveEffect does not exist in the Set corresponding to the current key
if(! dep.has(activeEffect)) { dep.add(activeEffect)// add to Set
activeEffect.deps.push(dep) // Add Effect deps as well
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
Copy the code
Trigger
The trigger method performs a ReactiveEffect and makes some type judgments internally, such as triggeroptypes. CLEAR only exists on maps and sets.
export function trigger(
target: object.type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if(! depsMap) {return
}
// Copy all effects that need to be executed into the Effects Set
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }The CLEAR type is present in the collectionHandlers of Map and Set
if (type === TriggerOpTypes.CLEAR) {
// The first argument to the Map forEach is the value, that is, the key pair to the Dep Set
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) { / / array
depsMap.forEach((dep, key) = > {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// key ! == undefined SET | ADD | DELETE
if(key ! = =void 0) {
// Add only the effect function for the current key
add(depsMap.get(key))
}
/ / ITERATE_KEY is a built-in identification variables ADD | DELETE | Map. The SET
switch (type) {
case TriggerOpTypes.ADD:
if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
// New index => length changes
add(depsMap.get('length'))}break
case TriggerOpTypes.DELETE:
if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break}}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()
}
}
/ / execution
effects.forEach(run)
}
Copy the code
Special handling of arrays
Although Proxy is used, array methods need special handling to avoid boundary cases that do not override array methods.
includes, indexOf,lastIndexOf
The special processing of these three methods is to be able to determine whether responsive data exists at the same time.
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ... args: unknown[]) {
const arr = toRaw(this)
for (let i = 0, l = this.length; i < l; i++) {
// Collect each subscript as a dependency
track(arr, TrackOpTypes.GET, i + ' ')}// Start with the current parameters
const res = method.apply(arr, args)
if (res === -1 || res === false) {
// If there is no result, change the parameter to the RAW value before executing
return method.apply(arr, args.map(toRaw))
} else {
return res
}
}
})
Copy the code
To ensure that both reactive and non-reactive values can be judged, it may be traversed twice.
Avoid circular dependencies
; (['push'.'pop'.'shift'.'unshift'.'splice'] as const).forEach(key= > {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ... args: unknown[]) {
pauseTracking()
const res = method.apply(this, args)
resetTracking()
return res
}
})
Copy the code
Array methods generally rely implicitly on lengh attributes, and in some cases may have circular dependencies (#2137).
conclusion
The above is the source analysis of Vue 3 responsive object and array interception. This paper only briefly analyzes the important interceptors in baseHandlers, and will bring the analysis of collectionHandlers later.