advantages
Reactive has been completely rewritten in VUe3.0, although the design idea is basically the same as vue2, except that Object. DefineProperty is rewritten as es6 proxy, but it also brings the following benefits
- You can listen for additions and deletions of objects without additional API support
- You can listen for array changes without additional rewriting of push, splice, and so on
- Supports monitoring of map, SET, WeakMap, and WeakSet set types
- Lazy recursion is used. Vue2 uses a forced recursive approach to listen on nested objects. Vue3, on the other hand, creates a proxy for a nested object that is read inside the object
The principle of analytic
Reactive, track, trigger and effect are mainly analyzed as follows
For details about proxy, see Ruan Yifeng Proxy
reactive
Create reactive objects and configure set, GET, has, and so on for them.
Due to the space problem, only the key codes are retained here, and some verification codes are removed.
All the code in: vue – next/packages/reactivity/SRC/reactive. Ts
function reactive(target: object) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler
, collectionHandlers: ProxyHandler
) {
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
Copy the code
CollectionHandlers are the agent for collections (Map,Set,WeakMap,WeakSet), and baseHandlers are the agent for objects and arrays. Here we focus on Set and GET in baseHandlers
get
//vue-next/packages/reactivity/src/baseHandlers.ts
function get(target: Target, key: string | symbol, receiver: object) {
const targetIsArray = isArray(target)
ArrayInstrumentations (); arrayInstrumentations ();
//1. If ['includes', 'indexOf', 'lastIndexOf'] is used to track each item in the array.
//2. If it is ['push', 'pop', 'shift', 'unshift', 'splice'], track will be paused during execution because push will trigger set twice, once to set value and once to change length. May lead to infinite recursion [detailed reference this issue] (https://github.com/vuejs/vue-next/issues/2137)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (
isSymbol(key)
? builtInSymbols.has(key as symbol)
: key === `__proto__` || key === `__v_isRef`
) {
return res
}
// Effect is collected here
if(! isReadonly) { track(target, TrackOpTypes.GET, key) }if (shallow) {
return res
}
//unref processing, returns the actual value of ref
if (isRef(res)) {
constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
}
// this is a lazy recursion
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
Copy the code
set
//vue-next/packages/reactivity/src/baseHandlers.ts
function set(target: object, key: string | symbol, value: unknown, receiver: object) :boolean {
const oldValue = (target as any)[key]
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)) {
// This triggers an update
if(! hadKey) {// If it is a new key, add
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// Otherwise it is modified
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
Copy the code
track
Dependencies are collected, stored in a global map object, and triggered on GET.
Format for:
{
target: {
key: [effect1, effect1, ...] }}Copy the code
Where activeEffect is assigned when effect creates the side effect function.
//vue-next/packages/reactivity/src/effect.ts
const targetMap = new WeakMap<any, KeyToDepMap>()
function track(target: object, type: TrackOpTypes, key: unknown) {
if(! shouldTrack || activeEffect ===undefined) {
return
}
let depsMap = targetMap.get(target)
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key)
if(! dep) { depsMap.set(key, (dep =new Set()))}if(! dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) } }Copy the code
trigger
//vue-next/packages/reactivity/src/effect.ts
function trigger(target: object, type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
// Get the corresponding dependency
const depsMap = targetMap.get(target)
if(! depsMap) {// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }if (type === TriggerOpTypes.CLEAR) {
// If it is cleared, all dependencies corresponding to the key are updated
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
// If it is length, only the code has dependencies on arr.length or those beyond the new length that need to be updated.
// For example: oldArr: [1, 2, 3] oldArr. Length = 2; Values that depend on 3 need to be updated
depsMap.forEach((dep, key) = > {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// Add a dependency for the key
if(key ! = =void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if(! isArray(target)) {// Here is the dependency to get the collection
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// if oldArr: [1,2,3] oldArr[3] = 4, the legth dependency is triggered
add(depsMap.get('length'))}break
case TriggerOpTypes.DELETE:
if(! isArray(target)) {// Get the dependency of the collection
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
// Get the dependency of the collection
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break}}Vue3's dependency updates are microtasks, and they are added to determine if there are any identical dependencies in the queue. (Mainly in run-time core/ SRC /scheduler.ts)
const run = (effect: ReactiveEffect) = > {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)
}
Copy the code
effect
Effect is the core of the connection to the above methods. This is where all the responses start to be injected.
Let’s take a look at the code implementation:
//vue-next/packages/reactivity/src/effect.ts
function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
// Computed is lazy and does not fire immediately, but only when it gets the corresponding value
if(! options.lazy) { effect() }return effect
}
function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
const effect = function reactiveEffect() :unknown {
if(! effect.active) {return options.scheduler ? undefined : fn()
}
if(! effectStack.includes(effect)) {// Effect deps is removed to prevent repeated caching
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
// Cache activeEffect, store depTarget on get
activeEffect = effect
// Execute the injected render function
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]}}}asReactiveEffect effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect =true
effect.active = true
effect.raw = fn
// Two-way cache is used for debugging.
effect.deps = []
effect.options = options
return effect
}
Copy the code
During vUE rendering
instance.update = effect(function componentEffect() {...// In this case, the template will get the value of reactive, ref and other reactive packages, so as to collect the effect wrapped by componentEffect. When data is reset, set will be triggered to update the effect and update the component
const subTree = (instance.subTree = renderComponentRoot(instance))
...
}, {
scheduler: queueJob,
allowRecurse: true
})
Copy the code