Original link: www.yingpengsha.com/vue3-0-xian…
preface
The overall responsiveness of Vue3.0 and Vue2.0 has not changed, but the implementation details have changed significantly. Vue3.0 decouples reactive systems from the body of the code, which means that Vue3.0’s reactive systems can be used as a separate library, just like RxJS. This article is designed to deepen the understanding of the responsivity principle of Vue3.0.
Design ideas
Although the responsive thinking of Vue3.0 and Vue2.0 has not changed, for the sake of review and explanation, we will reorganize the responsive system design of Vue
What is reactive
When object A changes, object B also changes
- Reactive programming: if the value of b or C changes, the value of A changes accordingly (a := b + c).
- Responsive layout: As the view window changes, so does the layout of elements within the view
- MVVM: As the Model changes, so does the View
Object A in Vue is the data, and object B is the render function of the view or Watch or computed
What does Vue’s responsive system need to do
- How do you know if the data has changed
- How do you know what data the response object depends on and establish dependencies
- How do I notify dependent response objects to respond when data changes
We then translate the above requirements into technical terms that we’ve all heard more or less before
- The data held
- Depend on the collection
- Distributed update
Realize the principle of
The data held
How do you know if the data has changed
An overview of the
How do you know that the data has changed, and how do you know that the data has been manipulated? The operations include (add, delete, modify, search, etc.). Vue2.0 uses object.defineProperty to hijack and customize setter and getter operations for objects, but there are some problems with this:
- The data defineProperty of the array data type cannot be hijacked directly, so it needs to be done by comparing hacks
- increase,deleteOperations cannot be captured in some scenarios, and in some scenarios cannot be hijacked, we must use them
$set
,$delete
These VUe-wrapped functions replace JS’s native value operations, increasing the mental cost - Waste of performanceSince there is no way to know which values need to be responsive, Vue2.0 will hold anything that can be held hostage in data, but in practice developers tend to change data in a much more granular manner, so this can result in a certain amount of wasted performance
Object.freeze()
Such operations can solve this problem to some extent.)
Vue3.0 uses Proxy to monitor data changes. By definition, Proxy is perfect for the purpose of data hijacking. Please refer to MDN introduction:
Proxy objects are used to create a Proxy for an object to intercept and customize basic operations (such as property lookup, assignment, enumeration, function calls, and so on).
In a sense, Proxy can be seen as an enhancement of Object.defineProperty, which has richer content that can be held hostage and solves the problems with using defineProperty described above:
- There is no need to do special hostage-taking for arrays, the Proxy takes care of it
- Add and delete operation Proxy can also be directly hijacked
- Because of some features of proxies, proxies can implement lazy hostage-taking without the need for deep hostage-taking of all values.
- And because the Proxy does not make changes to the data source, you can ensure that there are not too many side effects
Implementation details
Vue3.0 uses the Composition API to hold data in a responsive manner. We will only talk about reactive(), the most typical
reactive
export function reactive<T extends object> (target: T) :UnwrapNestedRefs<T>
export function reactive(target: object) {// If the target data has beenreadonly(), returns directly without reactive processingif (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
// Call a generic function for reactive processing
return createReactiveObject(
target, // Target data
false.// Whether to perform read-only operations
mutableHandlers, // Object/Array type of proxy processor
mutableCollectionHandlers // Map/Set/WeakMap/WeakSet proxy processor)}Copy the code
createReactiveObject
function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>) {
// If it is not an object, throw an error and return
if(! isObject(target))return target
// If the object is already responsive, return it directly, but if readonly() is already responsive, do not return it and continue execution
if(target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]))return target
Readonly and Reactive have one cache each
const proxyMap = isReadonly ? readonlyMap : reactiveMap
// If the object has already been proxied, it is fetched directly from the cache
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// Determine whether the target object is special or does not need to be hijacked, if so, return directly
Object/Array => targeType.mon, Map/Set/WeakMap/WeakSet => targeType.collection
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
WeakMap/WeakSet Creates a Proxy, if the target object type is Map/Set/WeakMap/WeakSet uses a Proxy processor specifically for collections, and vice versa
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// Cache
proxyMap.set(target, proxy)
/ / return
return proxy
}
Copy the code
mutableHandlers
Object/Array agent processor
Some constant value judgments in reactive situations, such as readOnly and shadow, are ignored for easy reading
export const mutableHandlers: ProxyHandler<object> = {
get(target: Target, key: string | symbol, receiver: object) {
/ /... Internal constant proxy
// ReactiveFlags.IS_REACTIVE = true
// ReactiveFlags.IS_READONLY = false
// ReactiveFlags.RAW = target
// Whether the target object is an array
const targetIsArray = isArray(target);
// Special handling when calling some specific array methods
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
/ / get the value
const res = Reflect.get(target, key, receiver);
// If it is some native built-in Symbol, or the value does not need to trace the direct return
if (
isSymbol(key)
? builtInSymbols.has(key as symbol)
: isNonTrackableKeys(key)
) {
return res;
}
// Rely on collection
track(target, TrackOpTypes.GET, key);
// If the value is already Ref(), whether to return a native Ref or its value depends on whether an array is currently accessed by a normal key
if (isRef(res)) {
constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key);return shouldUnwrap ? res.value : res;
}
// If it is an object, then it is held hostage (lazy-response source)
if (isObject(res)) {
return reactive(res);
}
// Return the result
return res;
},
set(target: object.key: string | symbol, value: unknown, receiver: object) :boolean {
/ / the old value
const oldValue = (target as any)[key];
// The new value removes possible responses
value = toRaw(value);
// If the old value is a Ref value, it is passed to Ref processing
if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = value;return true;
}
// There is no corresponding key value
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key);
/ / set the value
const result = Reflect.set(target, key, value, receiver);
// Send updates
if (target === toRaw(receiver)) {
if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value); }else if(hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue); }}return result;
},
deleteProperty(target: object.key: string | symbol): boolean {
const hadKey = hasOwn(target, key);
const oldValue = (target as any)[key];
const result = Reflect.deleteProperty(target, key);
// Send updates
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
}
return result;
},
has(target: object.key: string | symbol): boolean {
const result = Reflect.has(target, key);
// If the value is not a native internal Symbol value, a dependent collection is performed
if(! isSymbol(key) || ! builtInSymbols.has(key)) { track(target, TrackOpTypes.HAS, key); }return result;
},
ownKeys(target: object) : (string | number | symbol)[] {
// Rely on collection
track(
target,
TrackOpTypes.ITERATE,
isArray(target) ? "length" : ITERATE_KEY
);
return Reflect.ownKeys(target); }};Copy the code
collectionHandlers
Some constant value judgments in reactive situations, such as readOnly and shadow, are ignored for easy reading
Map/Set/WeakMap/WeakSet type WeakSet proxy processor Because the above four types of modified values are modified by functions, so the proxy function only intercepts the GET method, which is used to intercept the operation function called by the response object, and then carries out specific dependency collection or update distribution
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) = > {
/ /... Internal constant proxy
// ReactiveFlags.IS_REACTIVE = true
// ReactiveFlags.IS_READONLY = false
// ReactiveFlags.RAW = target
// Use the corresponding encapsulated functions for processing
return Reflect.get(
hasOwn(mutableInstrumentations, key) && key in target
? mutableInstrumentations
: target,
key,
receiver
)
}
}
// Function proxy
const mutableInstrumentations: Record<string.Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key)
},
get size() {
return size((this as unknown) as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false.false)}/ / get the value
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false
) {
// Native objects
target = (target as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
/ / primary key
const rawKey = toRaw(key)
// If it is reactive, the reactive key is dependent collected
if(key ! == rawKey) { track(rawTarget, TrackOpTypes.GET, key) }// Dependency collection on native keys
track(rawTarget, TrackOpTypes.GET, rawKey)
// If the target collection has, call HAS for dependency collection, because get() implicitly depends on HAS and returns a responsive key
const { has } = getProto(rawTarget)
if (has.call(rawTarget, key)) {
return toReactive(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return toReactive(target.get(rawKey))
}
}
function size(target: IterableCollections, isReadonly = false) {
// Rely on collection, key value for Symbol inside Vue ('iterate')
target = (target as any)[ReactiveFlags.RAW]
track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
}
function add(this: SetTypes, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
// Call native HAS
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
// If it does not exist, add it and send an update
if(! hadKey) { target.add(value) trigger(target, TriggerOpTypes.ADD, value, value) }return this
}
function set(this: MapTypes, key: unknown, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
const { has, get } = getProto(target)
// Check whether the corresponding key already exists based on the incoming key and the real rawKey that the key may have
let hadKey = has.call(target, key)
if(! hadKey) { key = toRaw(key) hadKey = has.call(target, key) }// Fetch the old value and set it
const oldValue = get.call(target, key)
target.set(key, value)
// If it is new, it triggers the new update, and if it is not, it triggers the Settings update
if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
}
function deleteEntry(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
// For the same set, check whether a corresponding key already exists by checking the incoming key and the real rawKey that the key may have
let hadKey = has.call(target, key)
if(! hadKey) { key = toRaw(key) hadKey = has.call(target, key) }// Fetch the old value and delete it
const oldValue = get ? get.call(target, key) : undefined
const result = target.delete(key)
// Trigger delete update
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function clear(this: IterableCollections) {
const target = toRaw(this)
consthadItems = target.size ! = =0
const result = target.clear()
// Trigger a clean update
if (hadItems) {
trigger(target, TriggerOpTypes.CLEAR, undefined.undefined.undefined)}return result
}
function forEach(
this: IterableCollections,
callback: Function, thisArg? : unknown) {
const observed = this as any
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
// Collect dependencies based on iterators
track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
// make the subset responsive
return target.forEach((value: unknown, key: unknown) = > {
return callback.call(thisArg, toReactive(value), toReactive(key), observed)
})
}
Copy the code
Depend on the collection
How do you know what data the response object depends on and establish dependencies
An overview of the
How to know what data the response object depends on is a further question of what data the response object uses. The general idea of Vue is like this. For example, I have a function fnA that uses B and C in data. To know that fnA uses B and C, we simply run fnA, wait for fnA to be acquired in B and C, and then establish a dependency between the two. There are some concepts in Vue2.0: Watcher, Dep, Target.
- Watcher stands for fnA
- Dep is an object in the setter for B that holds the Watcher collection
- Target is Watcher, which is currently doing dependency collection
New Watcher(fnA) => **target = current Watcher and fnA => fnA => fnA Vue3.0 has a different implementation. Because Vue3.0 no longer makes intrusive changes or hijks to data, Vue3.0 has a separate static variable store dependency. This variable is called targetMap and introduces a new concept called effect, which is similar to the **Watcher ** of Vue2.0, but with a shift in concept from listener to side effect, which refers to the side effect that occurs when the corresponding dependency value changes
The data type
The data types are as follows: The targetMap key points to Data, the KeyToDepMap key points to the ‘A’ and ‘B’ keys, and the value is the Watcher set in Effect
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
Copy the code
Implementation details
Effect
Effect is essentially the Watcher of Vue2.0, but it does a relatively simple job
export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
// If the function passed in is already an effect, the original function is removed and processed
if (isEffect(fn)) { fn = fn.raw }
// Create reactive side effects
const effect = createReactiveEffect(fn, options)
// Computed is lazy if it is not a side effect of laziness and runs directly and relies on collection
if(! options.lazy) { effect() }return effect
}
Copy the code
createReactiveEffect
function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
// Returns the wrapped side effect function
const effect = function reactiveEffect() :unknown {
// Side effect function core, more on that later
} as ReactiveEffect
// Some static attribute definitionseffect.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
reactiveEffect
function reactiveEffect() :unknown {
// If the side effect is already paused, its scheduler is executed first before the function body is run
if(! effect.active) {return options.scheduler ? undefined : fn()
}
// If the current side effect is not entered at runtime
if(! effectStack.includes(effect)) {// Clear the old dependencies first
cleanup(effect)
try {
// Enable dependency collection
enableTracking()
// Add to the running side effects stack
effectStack.push(effect)
// confirm the current side effect, target in Vue2.0
activeEffect = effect
// Execute the function
return fn()
} finally {
// Exit the stack
effectStack.pop()
// Turn off dependency collection
resetTracking()
// Pass the current side effect to the previous one or empty it
activeEffect = effectStack[effectStack.length - 1]}}}Copy the code
track
Is called in the data-held GET/HAS/ownKeys
export function track(target: object.type: TrackOpTypes, key: unknown) {
// If no collection is currently in progress, exit
if(! shouldTrack || activeEffect ===undefined) {
return
}
// Retrieve the object's KeyToDepMap, if not, create a new one
let depsMap = targetMap.get(target)
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Retrieve the dependency set for the corresponding key value, if not, create a new one
let dep = depsMap.get(key)
if(! dep) { depsMap.set(key, (dep =new Set()))}// If there is no current queue in the dependency, enter it to prevent repeated dependency setting
if(! dep.has(activeEffect)) {// Two-way dependency ensures consistency between old and new dependencies
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
Copy the code
Distributed update
How do I notify dependent response objects to respond when data changes
An overview of the
Distributing updates is the easiest part of the three, but Vue3.0 implements more details than Vue2.0, simply fetching the corresponding set of side effects from the dependency when the value changes and triggering the side effects **. We can see the call to the update trigger in the set/deleteProperty in the data hostage above.
Implementation details
trigger
export function trigger(
target: object.type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
// Return the KeyToDepMap of the object to which the response value is located
const depsMap = targetMap.get(target)
if(! depsMap) {// never been tracked
return
}
// Set of side effects that need to be triggered
const effects = new Set<ReactiveEffect>()
// Functions added to the collection can be seen below when triggered
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
// Pass in a collection of side effects
if (effectsToAdd) {
// Iterate over the set of incoming side effects
effectsToAdd.forEach(effect= > {
// Add a value to the set of side effects that are about to trigger if the side effect is not currently executing (preventing an endless loop of repeated calls) or allows recursive calls
if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }if (type === TriggerOpTypes.CLEAR) {
// If the corresponding modification operation is, such as new Set().clear()
// Add all subvalues of the side effects to the side effects queue
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
// If you change the length of the array, it means that the values after the new length are changed, and the corresponding side effects of these subscripts are queued
depsMap.forEach((dep, key) = > {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// Modify value, new value, delete value
// Add the side effects of the corresponding value to the queue
if(key ! = =void 0) {
add(depsMap.get(key))
}
// Add/delete corresponds to other side effects that need to be triggered (e.g. length dependent side effects, iterator dependent side effects)
switch (type) {
/ / new
case TriggerOpTypes.ADD:
// Add means the length has changed, firing the side effect function corresponding to the iterator and length
if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
add(depsMap.get('length'))}break
case TriggerOpTypes.DELETE:
// Same as above, because the array delete operation is special, it does not appear
if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
// a function that depends on iterators (e.g., calling new Map().foreach () equals a variant dependence on set)
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break}}// Call the scheduler first or the side effect itself
const run = (effect: ReactiveEffect) = > {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)
}
Copy the code