This is the second day of my participation in the wenwen Challenge
Effect is similar to the watch in vue2 source code, observer/subscriber. In the following procedure, effect is the observer, Target’s property value is the observed, effect looks at target’s property value, and target’s property value is changed to notify Effect. Be sure to view the source code in this way.
Side effect
Side effects are a relatively obscure concept, originally derived from “pure functions,” an early concept in programming, as you can see here and here. Features of pure functions:
- It should always return the same value. No matter how many times the function is called, whether it is called today, tomorrow, or sometime in the future.
- Self-contained (without using global variables).
- It should not modify the state of the program or cause side effects (modifying global variables).
So, the side effects of pure functions are:
A function that does something else besides returning a certain value is called a “side effect.”
Console.log (123), for example, has a side effect. It returns undefined as the main function, but we don’t need its main function, its side effect is to print 123 on the console, we need its side effect.
Side effects in Vue 3 So let’s start with “main effects.” In the Vue world, the view layer and the DOM layer are two different things, although some people think they are. The main function of change-responsive data is that the changed data can be rendered to the view layer. But basically that’s the most important thing about the front end, and the side effects are the other ripple effects of reactive data changes, and the subsequent logic, and these ripple effects are called side effects. In medicine, side effects tend to be adverse reactions, but in Vue3 they are not. The title “remove side effects” does not mean that side effects should be removed because they are undesirable, but Vue3 provides a way to remove side effects at any time.
The main side effects are: DOM update, watchEffect, Watch, computed…
Effect and Target mapping
// Store the effect Set
type Dep = Set<ReactiveEffect>
// Any indicates the attribute of the monitored object, which corresponds to the Dep
// Stores all properties and dePs of monitored objects
type KeyToDepMap = Map<any, Dep>
/ / any target
const targetMap = new WeakMap<any, KeyToDepMap>()
Copy the code
The purpose of this code is to use Proxy monitoring as a target, and every time a handler is triggered it is an attribute dimension, so you need to maintain a mapping of attributes to the Effect collection.
Dep == “effect collection
KeyToDepMap === all attributes of target correspond to Dep
TargetMap === All attributes of target
To modify the corresponding attribute of target, the effect corresponding to the attribute can be found through the above mapping to trigger execution.
The type of effect
export interface ReactiveEffect<T = any> {
(): T
// effect flag property
_isEffect: true
// Unique identifier
id: number
active: boolean
// The original function
raw: () = > T
// A collection of observers associated with the effect's response data
// Effect is the same object as the collection of observers to which the corresponding response data points
// The purpose is to remove the effect directly from the DEPS when it needs to be separated from the response data, and also remove the effect from the corresponding observer set of the response data, since it points to the same data
deps: Array<Dep>
// effect configuration options
options: ReactiveEffectOptions
}
The active property, when true, is aware of the effect dependency and executes the onStop event
// Stop is triggered inside unmountComponent
// So stop is triggered when the component dies, and if active is true it clears the dependency trigger onStrop
stop(effect: ReactiveEffect) {
if (effect.active) {
cleanup(effect)
if (effect.options.onStop) {
effect.options.onStop()
}
effect.active = false}}Copy the code
As you can see from the effect creation function below, when active is false, effect is a normal function passed in or undefined, with no listening dependencies
Effect configuration property
export interface ReactiveEffectOptions {
// Whether to execute immediatelylazy? : boolean// Scheduler, whether to customize scheduling effectscheduler? :(job: ReactiveEffect) = > void
// Whether to collect the listener functiononTrack? :(event: DebuggerEvent) = > void
// Whether to trigger the listener functiononTrigger? :(event: DebuggerEvent) = > void
// Whether to stop the listener functiononStop? :() = > void
}
// Event type
export type DebuggerEvent = {
// effect
effect: ReactiveEffect
// Listen to the object
target: object
// Type, which depends on the type, such as ADD, DELETE, CLEAR
type: TrackOpTypes | TriggerOpTypes
// Porxy listener properties
key: any
} & DebuggerEventExtraInfo
exportinterface DebuggerEventExtraInfo { newValue? : any oldValue? : any oldTarget? :Map<any, any> | Set<any>
}
Copy the code
Effect constructor
export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
// If fn is a listener, get the original function and recreate it
// Effect returns a new monitor function each time
if (isEffect(fn)) {
fn = fn.raw
}
// The core is createReactiveEffect
const effect = createReactiveEffect(fn, options)
The lazy attribute identifies whether it is executed once by default
if(! options.lazy) { effect() }return effect
}
Copy the code
Pass in a function that returns an effect constructor
3. CreateReactiveEffect function
function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
// Create a function of type ReactiveEffect
const effect = function reactiveEffect() :unknown {
// If active is false, dependency collection will not take place
if(! effect.active) {return options.scheduler ? undefined : fn()
}
// effectStack refers to the effect stack
// If there is no effect in the effectStack, execute
// To prevent cyclic dependencies
// If effect1 triggers effect2, effect2 triggers effect2, effect2 triggers effect1, but effect1 is still in the effectStack, it will not enter
if(! effectStack.includes(effect)) {// Clear the monitoring data in effect
// And clear the effect observer from the monitoring data dependency
// Clear bidirectionally, mainly through the deps attribute
cleanup(effect)
try {
// Push a shouldTrack to the trackStack
ShouldTrack is mainly whether to rely on collecting flag bits
enableTracking()
// Push an effect onto the effectStack
effectStack.push(effect)
// Active effect points to this effect
activeEffect = effect
// Execute fn to fire the getter corresponding to the internal monitor data to collect the effect dependency
return fn()
} finally {
// Pop this effect
effectStack.pop()
Popup shouldTrack / /
resetTracking()
activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
// Set the base properties of effect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
// record the last shouldTrack state
// and now set to false
// This function is executed when wraning, for example
export function pauseTracking() {
trackStack.push(shouldTrack)
shouldTrack = false
}
// record the last shouldTrack state
// And now dependencies can be collected because fn, fetch is performed
export function enableTracking() {
trackStack.push(shouldTrack)
shouldTrack = true
}
// effect completes, then shoudTrack is set to its previous state
export function resetTracking() {
const last = trackStack.pop()
shouldTrack = last === undefined ? true : last
}
Copy the code
The original function fn is redefined as effect, and its main function is similar to watch as an observer
The cleanup function
Effect: To clear the effect dependency
function cleanup(effect: ReactiveEffect) {
// Get the observed effect set associated with this effect
// The deP associated with this effect points to the same storage address
const { deps } = effect
if (deps.length) {
// Remove this effect from the deP
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
// And remove each term of deps
deps.length = 0}}Copy the code
Deps [I]. Delete removes the connection between observed and effect, and DEps. Length =0 removes the connection between effect and observed.
Track function
What it does: Depends on collection
export function track(target: object, type: TrackOpTypes, key: unknown) {
// Return if no dependent collection is performed or if the target observer is undefined
if(! shouldTrack || activeEffect ===undefined) {
return
}
// Gets the attributes of the target and the observer collection box set
DepsMap
, key is tarfet
,>
let depsMap = targetMap.get(target)
// If it does not exist, recreate it
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Get the dependency set corresponding to the key
let dep = depsMap.get(key)
// If it does not exist, recreate it
if(! dep) { depsMap.set(key, (dep =new Set()))}// Check whether the effect observer exists
if(! dep.has(activeEffect)) {// Add this effect to the deP of the corresponding attribute value
dep.add(activeEffect)
// The deps in this effect also adds the DEP of the observed attribute value
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
// Trigger the onTrack function
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
Copy the code
It mainly collects the target targetEffect, and the DEPS of this targetEffect also adds the DEP corresponding to the observed attribute value to do double mapping
Trigget function
Function: notifies the dependency to execute the callback function
export function trigger(target: object, type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
// Get all observers associated with target
const depsMap = targetMap.get(target)
if(! depsMap) {// never been tracked
return
}
// Add a dependency Set variable to ensure uniqueness
const effects = new Set<ReactiveEffect>()
// Add function by name
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
// Avoid circular dependencies
if(effect ! == activeEffect) { effects.add(effect) } }) } }// Clear refers to target clearing all events triggered by attributes
// Target = null
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// Iterate over the observation set of all properties and add it to Effects
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
// Monitor array length changes. Pop, shift, push, unshift, etc
depsMap.forEach((dep, key) = > {
// Add all attributes with length and key greater than or equal to length to effects
// Keys smaller than length are collected automatically
Array.length (); array. length ()
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// Add activeEffect for the deP of the corresponding attribute
if(key ! = =void 0) {
add(depsMap.get(key))
}
// The ITERATE_KEY collection function is the fifth parameter of the proxy handler, which monitors the collection of Object attributes, such as object. keys, for(key in target), etc
// function ownKeys(target: object): (string | number | symbol)[] {
// track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
// return Reflect.ownKeys(target)
// }
// also run for iteration key on ADD | DELETE | Map.SET
// Delete add to change object and array keys length and length
// For example, when using watch to monitor target deeply, it will perform recursive monitoring of key in target, so ownKeys will be triggered. When target deletes and adds attributes, it will also monitor
// May have other effects, to be continued after use
constisAddOrDelete = type === TriggerOpTypes.ADD || (type === TriggerOpTypes.DELETE && ! isArray(target))if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
const run = (effect: ReactiveEffect) = > {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
// If there is a scheduler, the scheduler is run
// Otherwise, effect is executed
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)
}
Copy the code
conclusion
Effect is equivalent to watch. There is a mapping table between the properties of target and effect. The acquisition of the property value of KeyToDepMap Target will trigger Track, and the modification of the property value of activeEffect Target will trigger trigger. At this point, the KeyToDepMap associated with the object is retrieved from targetMap using target, and the DEP is retrieved from KeyToDepMap using the attribute key. The deP is added to the execution queue by traversing the DEP