introduction
Introduce a fewAPI
When we found that it often appears in iteffect
、track
andtrigger
, although I'll briefly saytrack
for dependency collection,trigger
to trigger updates. But after all, I didn't see the specific implementation and I felt unsure. Now we can find out.
1. ReactiveEffect
1. Related global variables
As mentioned earliereffect
, that'sReactiveEffect
Examples of . Some important global variables were used.
-
targetMap
: Weak map to target objecttarget
forkey
, the dependency set it collectsdepsMap
is a value, so through the target objecttarget
All corresponding dependencies can be obtained; -
activeEffect
: Currently activeeffect
, and will be collected later; -
shouldTrack
: Used as a flag to pause and resume dependency collection; -
trackStack
:historyshouldTrack
record stack.
targetMap
contrastreactive
The chapter mentionedproxyMap
:
- Both are weak mappings;
- All target objects
target
forkey
; -
targetMap
There is only one overall situation;proxyMap
There are four types, corresponding toreactive
、shallowReactive
、readonly
、shallowReadonly
; - one
target
In aproxyMap
There is at most one corresponding agentproxy
,thereforeproxyMap
The value of a singleproxy
object; - one
target
It can be depended on by manydep
,thereforetargetMap
The value of the datasetMap
。
const targetMap = new WeakMap<any, KeyToDepMap>() export let activeEffect: ReactiveEffect | undefined export let shouldTrack = true const trackStack: boolean[] = []
And functions that control pause and restore dependency collection:
// Pause collectionexport function pauseTracking() { (shouldTrack) shouldTrack = false } // Recovery collectionexport function enableTracking() { (shouldTrack) shouldTrack = true } // Reset to the previous stateexport function resetTracking() { const last = () shouldTrack = last === undefined ? true : last }
2. class statement
Initialize in the constructorfn
( implementrun()
call ) , schedulerscheduler
, and passrecordEffectScope
to record the scope of an instance; declare some instance properties, andrun
、stop
Two methods:
-
active
:boolean
Type, indicating the currenteffect
Whether it works; -
deps
:currenteffect
dependency; -
parent
: Point to the previous activeeffect
, form a linked list; -
computed
: Optional, incomputed
The function is obtainedComputedRefImpl
Insideeffect
Have this attribute; -
allowRecurse
, optional, indicating whether self-calling is allowed; -
deferStop
: Private, optional, indicatingstop()
Whether to delay execution; -
onStop
: optional, function, in executionstop()
Will be called whenonStop
; onTrack
-
onTrigger
: These twolistener
For debugging purposes, it is fired when dependency collection and responsive updates are respectively; -
run:
effect
The most core method. -
stop
: CalledcleanupEffect
leteffect
Stop working, if it isstop
Currently activeeffect
, that is, stop yourself, and you willdeferStop
Adjust totrue
, thereby delaying the stop time; triggeringonStop
;Willactive
Adjust tofalse
。
export class ReactiveEffect<T = any> { active = true deps: Dep[] = [] parent: ReactiveEffect | undefined = undefined /** * Can be attached after creation * @internal */ computed?: ComputedRefImpl<T> /** * @internal */ allowRecurse?: boolean /** * @internal */ private deferStop?: boolean onStop?: () => void // dev only onTrack?: (event: DebuggerEvent) => void // dev only onTrigger?: (event: DebuggerEvent) => void constructor( public fn: () => T, public scheduler: EffectScheduler | null = null, scope?: EffectScope ) { recordEffectScope(this, scope) } run() { if (!) { return () } // Currently active effect let parent: ReactiveEffect | undefined = activeEffect let lastShouldTrack = shouldTrack // If the currently active effect is this effect itself, then return it directly while (parent) { if (parent === this) { return } parent = } // Active effects are successively formed into a linked list, connected by the parent attribute try { = activeEffect activeEffect = this shouldTrack = true trackOpBit = 1 << ++effectTrackDepth if (effectTrackDepth <= maxMarkerBits) { // Traversal Set the effect in it to captured tracked initDepMarkers(this) } else { // Level overflow clears the current side effects cleanupEffect(this) } // The incoming fn is called return () } finally { // Because there is a return in front, it is executed when an exception occurs in the code block of try if (effectTrackDepth <= maxMarkerBits) { // This method traverses , deletes the outdated effect and adds the uncaught effect // effect is the dep finalizeDepMarkers(this) } trackOpBit = 1 << --effectTrackDepth // Restore some states activeEffect = shouldTrack = lastShouldTrack = undefined // If delay stop is set, execute stop and perform delay cleanup if () { () } } } // Clear side effects stop() { // stopped while running itself - defer the cleanup if (activeEffect === this) { = true } else if () { cleanupEffect(this) if () { () } = false } } }
3. cleanupEffect
cleanupEffect
Used to remove side effects. Receive oneeffect
, traversaland remove side effects one by one
effect
. Then clear。
function cleanupEffect(effect: ReactiveEffect) { const { deps } = effect if () { for (let i = 0; i < ; i++) { deps[i].delete(effect) } = 0 } }
2. Effect function
1. Related ts types
effect
There are several related types of functions:
-
ReactiveEffectOptions
:effect
One of the entry parameters types of the function; -
ReactiveEffectRunner
: is a function and haseffect
type of attribute;
export interface DebuggerOptions { onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void } export interface ReactiveEffectOptions extends DebuggerOptions { lazy?: boolean scheduler?: EffectScheduler scope?: EffectScope allowRecurse?: boolean onStop?: () => void } export interface ReactiveEffectRunner<T = any> { (): T effect: ReactiveEffect }
2. Function declaration
effect
The function has two parameters:
-
fn
: is a function that is used to createReactiveEffect
Example_effect
; -
options
: Optional, for coverage_effect
attributes on.
export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions ): ReactiveEffectRunner { // Process fn if ((fn as ReactiveEffectRunner).effect) { fn = (fn as ReactiveEffectRunner). } // Create a _effect based on fn const _effect = new ReactiveEffect(fn) if (options) { // Use options to override the properties on _effect extend(_effect, options) if () recordEffectScope(_effect, ) } // Without lazy , then _effect executes run() immediately if (!options || !) { _effect.run() } // runner: Get _effect.run and hang the effect attribute, wrap it into ReactiveEffectRunner type const runner = _effect.(_effect) as ReactiveEffectRunner // The effect attribute refers to the _effect itself, which is convenient for using runner to call run and stop = _effect // Return to runner return runner }
3. stop function
stop
Used for clearingeffect
. Enter the ginseng asReactiveEffectRunner
;
export function stop(runner: ReactiveEffectRunner) { () }
3. Track dependency collection
1. track
Always talkingtrack
Conduct dependency collection, here we see how it is done.
- To target object
target
forkey
,depsMap
fortargetMap
value;target
ofkey
forkey
,usecreateDep()
Create dependenciesdep
as value, stored intarget
CorrespondingdepsMap
middle. - pass
trackEffects(dep, eventInfo)
To collect side effects.
// Global variable targetMapconst targetMap = new WeakMap<any, KeyToDepMap>() export function track(target: object, type: TrackOpTypes, key: unknown) { if (shouldTrack && activeEffect) { let depsMap = (target) if (!depsMap) { (target, (depsMap = new Map())) } let dep = (key) if (!dep) { (key, (dep = createDep())) } const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined trackEffects(dep, eventInfo) } }
2. createDep
usecreateDep
Create a newdep
. You can see,dep
It's aSet
Instance, and two attributes are added:
-
w
:wasTracked
The first letter of the current dependency is collected; -
n
:newlyTracked
The first letter of , indicates whether the current dependency is newly collected.
export const createDep = (effects?: ReactiveEffect[]): Dep => { const dep = new Set<ReactiveEffect>(effects) as Dep = 0 = 0 return dep }
3. trackEffects
trackEffects
Used to collect side effects. Mainly put the currently activeactiveEffect
join indep
, and inAdd all dependencies affected by this side effect.
export function trackEffects( dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { let shouldTrack = false if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { |= trackOpBit // set newly tracked shouldTrack = !wasTracked(dep) } } else { // Full cleanup mode. shouldTrack = !(activeEffect!) } // The current dependency dep has not been captured yet / When the current dependency dep is in the current dependency dep and there are no currently active side effects, // Add the currently active side effects effect to dep, and add dep to the dependency set affected by side effects if (shouldTrack) { (activeEffect!) activeEffect!.(dep) if (__DEV__ && activeEffect!.onTrack) { activeEffect!.onTrack({ effect: activeEffect!, ...debuggerEventExtraInfo! }) } } }
4. Summary
In a difficult sentence, relying on collection is to take the side effects of current activenessactiveEffect
Save global variablestargetMap
In (target
CorrespondingdepsMap
) middle (target
ofkey
) correspondingdep
(Type isSet
) and put thisdep
Join to the recipientactiveEffect
All dependencies affected by side effectsin the list.
4. trigger
Triggering updates is actually triggering side effects, so this subsection decides totrack
In reverse order.
1. triggerEffect
triggerEffect
Trigger side effects to update. Side effects of triggering updateeffect
When self-calling is allowed, and is not currently active side effects, through the schedulerscheduler
Execute side effects or directlyrun
, is where updates are actually triggered.
function triggerEffect( effect: ReactiveEffect, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { if (effect !== activeEffect || ) { if (__DEV__ && ) { (extend({ effect }, debuggerEventExtraInfo)) } // Where to actually trigger updates if () { () } else { () } } }
2. triggerEffects
Receive onedep
and additional information for debugging. Traversaldep
In-houseeffect
, use one by onetriggerEffect
to perform side effects. The source code is a bit of honey here.
export function triggerEffects( dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { // spread into array for stabilization const effects = isArray(dep) ? dep : [...dep] // Are the two mutually exclusive, but the operations performed the same? And why not write it in a for...of...? for (const effect of effects) { if () { triggerEffect(effect, debuggerEventExtraInfo) } } for (const effect of effects) { if (!) { triggerEffect(effect, debuggerEventExtraInfo) } } }
3. trigger
Always said beforetrigger
Trigger update, it is actually known now, but it is actuallytriggerEffect
to perform side effects to achieve updates.
Here is to create adeps
Array, according totarget
、key
and the operation type that triggers updatestype
etc. to get all related parametersdep
, put indeps
. Take it out againdeps
All indep
All ineffect
, put ineffects
In the list, bytriggerEffects(effects)
to trigger all related side effects and finally achieve updates.
It should be noted that for arrays:
- Revise
length
Properties will cause updates to all dependencies of the array; - Revising the array will cause
length
Updates of attribute-related dependencies becauselength
The value of .
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { const depsMap = (target) if (!depsMap) { // never been tracked return } // Used to gather all related dependencies let deps: (Dep | undefined)[] = [] if (type === ) { // Call the clear method of Set and Map instances, which will trigger all related side effects // collection being cleared // trigger all effects for target deps = [...()] } else if (key === 'length' && isArray(target)) { // When the target object is an array, and the length attribute is modified, all related side effects will be triggered ((dep, key) => { if (key === 'length' || key >= (newValue as number)) { (dep) } }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { ((key)) } // also run for iteration key on ADD | DELETE | switch (type) { case : if (!isArray(target)) { ((ITERATE_KEY)) if (isMap(target)) { ((MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // Changes to array subscript members will cause updates related to the length attribute // new index added to array -> length changes (('length')) } break case : if (!isArray(target)) { ((ITERATE_KEY)) if (isMap(target)) { ((MAP_KEY_ITERATE_KEY)) } } break case : if (isMap(target)) { ((ITERATE_KEY)) } break } } const eventInfo = __DEV__ ? { target, type, key, newValue, oldValue, oldTarget } : undefined if ( === 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) { (...dep) } } // Here triggerEffects accepts parameter type Set, before it is an array if (__DEV__) { triggerEffects(createDep(effects), eventInfo) } else { triggerEffects(createDep(effects)) } } }
5. Summary
1. Dependency collection
targetMap
There isdepsMap
(bytarget
forkey
);depsMap
There are many indep
(bytargetMap
ofkey
forkey
); simply understood as: when compiling, according totarget
andkey
, create side effects, willactiveEffect
Point to newly created side effects and store them in related dependenciesdep
The process in it is relying on collection.
2. Trigger update
In turn, triggertarget
、key
Relateddep
All related side effects are passed through the()
or()
to implement updates.
The above is the detailed content of the source code analysis of the Vue3 series effect and ReactiveEffect track trigger. For more information about Vue3 effect and ReactiveEffect track trigger, please follow my other related articles!