Vue2 response formula principle
Those of you who have studied Vue2 know that the responsivity principle is the hijacking of data by Object.defineProperty, coupled with subscription publishing, to achieve the response of data.
Object.defineproperty has several disadvantages.
-
Initialization requires hijacking by traversing all attributes of the object, and recursion if the object is nested. Cause initialization to consume some resources for recursive traversal.
-
It can be inferred from the above that Vue2 cannot hijack new or deleted object attributes and needs to be operated by vue. set and vue. delete.
-
Each caller generates a Watcher, resulting in memory footprint.
-
Unable to hijack Set or Map objects.
Vue3 response principle
To solve the above problems, Vue3 uses ES6 native Proxy for data Proxy.
The basic usage of Proxy is as follows:
const reactive = (target) = > {
return new Proxy(target, {
get(target, key) {
console.log("get: ", key);
// return Reflect.get(target, key);
return target[key];
},
set(target, key, value) {
console.log("set: ", key, "=", value);
// Reflect.set(target, key, value);
target[key] = value;
returnvalue; }}); };var a = reactive({ count: 1 });
console.log(a.count);
a.count = 2;
console.log(a.count);
/ / the log output
// get: count
/ / 1
// set: count = 2
// get: count
/ / 2
Copy the code
This allows you to detect changes in the data. All you need to do next is collect the dependencies in GET, and set notifies the dependency updates.
Then you need the effect, track, and trigger methods.
The effect function passes in a callback that executes immediately and automatically relies on responsive data.
Track is executed in proxy GET to establish dependencies.
Trigger When responsive data changes, corresponding functions are found and executed according to dependencies.
The code implementation is as follows:
const reactive = (target) = > {
return new Proxy(target, {
get(target, key) {
console.log("[proxy get] ", key);
track(target, key);
// return Reflect.get(target, key);
return target[key];
},
set(target, key, value) {
console.log("[proxy set] ", key, "=", value);
// Reflect.set(target, key, value);
target[key] = value;
trigger(target, key);
returnvalue; }}); };// It is used to store the fn passed in by effect to facilitate track to find the corresponding FN
const effectStack = [];
// Used to store the relationship between reactive objects and fn
/ / {
// target: {
// key: [fn, fn];
/ /}
// }
const targetMap = {};
const track = (target, key) = > {
let depsMap = targetMap[target];
if(! depsMap) { targetMap[target] = depsMap = {}; }let dep = depsMap[key];
if(! dep) { depsMap[key] = dep = []; }// Establish dependencies
const activeEffect = effectStack[effectStack.length - 1];
dep.push(activeEffect);
};
const trigger = (target, key) = > {
const depsMap = targetMap[target];
if(! depsMap)return;
const deps = depsMap[key];
// Based on the dependencies, find fn and re-execute
deps.map(fn= > {
fn();
});
};
const effect = (fn) = > {
try {
effectStack.push(fn);
fn();
} catch(error) { effectStack.pop(fn); }};var a = reactive({ count: 1 });
effect(() = > {
console.log("[effect] ", a.count);
});
a.count = 2;
/ / the log output
// [proxy get] count
// [effect] 1
// [proxy set] count = 2
// [proxy get] count
// [effect] 2
Copy the code
The above code is not Vue3 source code, but Vue3 responsive principle, compared to Vue2 to be more simple.
The execution sequence is
-
Call reactive proxy responsive objects;
-
Calling effect saves fn to the effectStack and triggers Proxy GET when fn is executed.
-
Track is triggered by get of Proxy to establish a relationship between data and FN.
-
Modify responsive data to trigger Proxy SET;
-
Trigger is triggered from the Proxy’s set to find the corresponding FN and execute.
Figure out the principle to look at the source code will be much simpler, let’s go to the source code.
Vue3 responsive source code
Vue3’s responsiveness is a standalone module, independent of the framework, and can even be used in React and Angular.
Reactive function in the packages/reactivity/SRC/reactive. Ts
// packages/reactivity/src/reactive.ts
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// ...
const proxy = new Proxy(
target,
/ / on the Set, a collection of Map use collectionHandlers (mutableCollectionHandlers)
// Normal objects use baseHandlers.
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// ...
return proxy
}
Copy the code
mutableHandlers
// packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
Copy the code
Look at get and set
// packages/reactivity/src/baseHandlers.ts
const get = /*#__PURE__*/ createGetter()
// ...
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// ...
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if(! isReadonly) {// Call track to establish dependencies
track(target, TrackOpTypes.GET, key)
}
// ...
return res
}
}
Copy the code
// packages/reactivity/src/baseHandlers.ts
const set = /*#__PURE__*/ createSetter()
// ...
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
) :boolean {
let oldValue = (target as any)[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) {// Invoke trigger to tell dependencies to re-execute
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// Invoke trigger to tell dependencies to re-execute
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
Copy the code
Now track
// packages/reactivity/src/effect.ts
export function track(target: object.type: TrackOpTypes, key: unknown) {
if(! isTracking()) {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 = createDep())) }const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
export function trackEffects(dep: Dep, debuggerEventExtraInfo? : DebuggerEventExtraInfo) {
// ...
if(shouldTrack) { dep.add(activeEffect!) activeEffect! .deps.push(dep) } }Copy the code
The upper part is similar to the logic we implemented ourselves, finding the deP if it doesn’t exist and creating it, except that Vue uses Map and Set (createDep returns Set).
Then there is the trackEffects, where the key code is that DEP and activeEffect save each other, so we just store the activeEffect in THE DEP.
Next look at the trigger called in the set.
// packages/reactivity/src/effect.ts
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) {// never been tracked
// Not collected by track, return directly
return
}
let deps: (Dep | undefined=) [] []if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// To clear dependencies, trigger all effects associated with target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
// Change the length of the array
depsMap.forEach((dep, key) = > {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// Execute when modifying, adding, or deleting attributes
if(key ! = =void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
// Effect to add iterator attributes to deps
switch (type) {
// ...}}// Select deps (targetMap[target][key])
// Take effect out of deps and execute it
// eventInfo is also passed in during development
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])}}}else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if(dep) { effects.push(... dep) } }if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
/ / execution effect
export function triggerEffects(dep: Dep | ReactiveEffect[], debuggerEventExtraInfo? : DebuggerEventExtraInfo) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if(effect ! == activeEffect || effect.allowRecurse) {if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
Copy the code
Trigger function seems to be very long, but in fact, it can be simplified into our example to understand, which is to take out the corresponding DEPS, iterate through the effect in DEPS and execute it.
The next step is to look at the implementation of the Effect function.
// packages/reactivity/src/effect.ts
export function effect<T = any> (fn: () => T, options? : ReactiveEffectOptions) :ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
// Call ReactiveEffect to wrap
const _effect = new ReactiveEffect(fn)
// ...
// Check whether options.lazy exists
// Lazy is true and not executed immediately
if(! options || ! options.lazy) { _effect.run() }const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
// can be attached after creationcomputed? :booleanallowRecurse? :booleanonStop? :() = > void
// dev onlyonTrack? :(event: DebuggerEvent) = > void
// dev onlyonTrigger? :(event: DebuggerEvent) = > void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null, scope? : EffectScope |null
) {
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
if(! effectStack.includes(this)) {
try {
// The current effect is stored in the effectStack during execution
// And assign to activeEffect
// Get at track
effectStack.push((activeEffect = this))
enableTracking()
// ...
return this.fn()
} finally {
// ...
resetTracking()
effectStack.pop()
const n = effectStack.length
// Remove the last activeEffect from the effectStack and continue execution
activeEffect = n > 0 ? effectStack[n - 1] : undefined}}}stop() {
// ...}}Copy the code
When we use effect, we wrap the function we pass in with ReactiveEffect, and if we don’t pass {lazy: true} we execute the run function immediately.
The run function simply assigns an activeEffect to the effectStack and then executes the callback we passed in.
The process of executing the callback triggers Proxy GET, which triggers Track for dependency collection.
Pop the activeEffect out of the effectStack and retrieve the last activeEffect to continue execution.
Why use the effectStack?
If we use computed in Effect, Vue needs to compute computed first.
ReactiveEffect is also invoked inside computed, so you need to store a computed effect into the effectStack, and pop it out of the effectStack when computed, Continue with our effect.
This completes the dependency collection, triggering the callback we passed in effect when the reactive data changes.
Modify responsive data Why do pages update automatically? Remember the setupRenderEffect introduced in the previous article “go deep into the Vue3 source code and learn the initialization process”?
This method also makes use of ReactiveEffect. When mounting, setupRenderEffect will be triggered and then patch will be triggered. In the process of patch, responsive data will be used to establish the dependency relationship. When the responsive data changes, setupRenderEffect will be re-executed, and then DIFF will be introduced. In the next article, DIFF will be expanded in detail.
conclusion
The above is Vue3’s responsive principle, as long as you understand the principle and can clearly describe it in your own language, the interview will certainly increase the success rate.
Well, that’s all for this article. If there is any mistake, I hope you can point it out in the comment section, thanks!
The next article will analyze the DIff algorithm of Vue3. If you are interested, don’t forget to follow me. We will learn and make progress together.