Liu Chongzhen, the front engineer of The Wedoctor cloud service team, is an atypical coder with a baby in his left hand and a home in his right.
- Previously: In the
Vue3 initialization
In the course of oursetupStatefulComponent
And leave oneTODO: the pit Reactive
, introduces: where the response is initialized. - We hit the break point at
reactive
Function calls here, take a look at the call stack, reviewVue3 initialization
Process, to connect with thisReactive
In this paper.
Let’s take a look at our demosetup
Let’s take this little piece of code and turn it overVue3.0
Is the response formula of.
A, Reactive
Reactive is implemented through createReactiveObject: Transforming a target into a reactive object
Look at the flag of the target object for easy code understanding
export interfaceTarget { [ReactiveFlags.SKIP]? :boolean // Skip to target[ReactiveFlags.IS_REACTIVE]? :boolean // Target is responsive[ReactiveFlags.IS_READONLY]? :boolean // Target is read-only[ReactiveFlags.RAW]? :any // Target corresponds to the original data source, without a reactive proxy
}
Copy the code
1.1 createReactiveObject
CreateReactiveObject is used to convert a target to a reactive object. The most important thing here is the new Proxy, which returns the desired reactive Proxy. Compared to the old defineProperty API, proxies can Proxy arrays and, with Reflect, perfectly implement Traps interception.
It’s also possible to Reflect trap intercepting actions (such as set in the following code) via a proxy target, but for more complex default actions, Reflect is more convenient. You’ll find the corresponding method on the Reflect object.
let proxy = new Proxy(target, handlers);
// set trap
let proxy = new Proxy(target, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
}
})
Copy the code
All uses of Proxy objects are in this form, except for the handler arguments.
Handlers are used to customize intercepting behavior based on the type of target, and baseHandlers are used for Object and Array interceptions, which we often use.
Note: Reactive’s input target must be of type Object. If you want to convert a primitive data type to a reactive object, you need ref, which can accept both primitive and reference types.
The baseHandlers block the get, set, deleteProperty, HAS and ownKeys operations on the target object.
1.2 createGetter
Handlers that define interception behavior are set to the proxy when the proxy object is created in the createReactiveObject method, and createGetter creates getter interceptors.
vue3.0
The array method has been hacked, stored inarrayInstrumentations
Hacks are mainly used to traverse search methods (indexOf, lastIndexOf, includes) and to change the array length (push, POP, Shift, unshift, splice).- Hack traversal search method is to traverse the elements in the array with the for loop through track once, according to the array subscript collection of each element dependencies;
- Hack changes the length of an array. The method of changing the array length will trigger get and set execution multiple times during execution, and each trap operation will trigger dependent collection and side effects distribution, such as an array operation, resulting in the render function patch multiple times, which is a problem.
// Change the array length method, which must trigger the.length property.
const arr = [1.2];
const proxy = new Proxy(arr, {
get(target, key, receiver) {
console.log('get', key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('set', key, value);
return Reflect.set(target, key, value, receiver); }}); proxy.unshift(3);
// 'get' 'unshift' - Trap unshift method
// 'get' 'length' - Trap length attribute
// 'get' '1' - get the last index of the array 1
// 'set' '2' = '1'; // 'set' '2' = '2'
// 'get' '0' - get the index 0 of the array
// 'set' '1' 1 - moves the value of the original subscript 0 back
// 'set' '0' 3 - set subscript 0 to 3
// 'set' 'length' 3 - Sets array length to 3
/ / 3
// This will cause a bug, as in the following DEMO. Push (1) immediately executes,.push will get length to collect dependencies, and then trigger when set length,
// Re-execute the.push(1) in effect to form a track-trigger loop. So by pauseTracking
// Stop collecting dependencies during the execution of the above methods and track-trigger with.length.
const arr = reactive([])
// watchEffect: Executes a function passed in immediately, traces its dependencies responsively, and rerunts the function when its dependencies change.
watchEffect(() = >{
arr.push(1)})Copy the code
Backtracking (); backtracking (); backtracking (); backtracking (); In this way, the execution of these methods does not have to re-collect and respond to the array elements.
track
Collect dependencies and store them in the global repository
Track-trigger is a pair of twin brothers. Track collects dependencies and trigger triggers dependencies. Let’s start with Track, which is the function that does dependency collection in the getter process.
If pauseTracking is set to global shouldTrack, shouldTrack should be set to false.
Since it is always said that dependencies are collected, who is the subject of the collection and what are the dependencies collected? The main (?) Collect sb.
As you can see in the figure below, tragetMap is a data response relational repository. Is a WeakMap type data structure. Key corresponds to the original data source, which can be data wrapped in reactive function (or an instance of computedRefImpl, etc.). Value is a data structure of the Map type. The key of a Map is a property of the data source, and the value of a Map is a data structure of the Set type (DEP Set), which stores the corresponding DEPS, that is, the collected dependencies. Therefore, we can understand that dependency is a kind of effect side effect. TragetMap, the global data response relation warehouse (subject), collects the function (object) that depends on the change of the responsive object and rerun to get the corresponding side effect performance. We can also understand that track is the producer of DEPS. Notice that there are many effects in DEPS.
One more detail in the track code is activeEffect.deps.push(DEP). Each dependency collected will maintain a DEPS for itself, recording the deP set that holds the dependency, which is used to record the interholding relationship between the effect and the dependency repository. . During effect execution, it is effective to keep the mutual holding relationship up to date by updating the holding relationship.
- Recursive processing, on-demand transformation,
reactive
When processing data, reactive processing continues only when the getter for the key is triggered to intercept the object
In version 2.x, the reactive transformation is a one-time recursive transformation during the initialization phase. 3.0 is optimized to continue the reactive conversion of the value of the current property only if the getter is fired and the value is of an object type. On demand transformation is done, declaring a responsive object that is dependency collected only when the getter that triggers the property is actually used. In addition, Proxy can only represent one layer, and the deep detection of internal objects needs to be handled by the developers themselves, which is also the reason for on-demand recursion here.
- through
Reflect
The return value
1.3 createSetter
CreateSetter creates setter interceptors for update distribution
Reflect.set Performs the original set operation, and triggers the trigger function conditionally
This trigger can trigger restrictions 1, 2 and 3
// createSetter
if (target === toRaw(receiver)) { / / limit 1
if(! hadKey) {2 / / restrictions
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) { 3 / / limitation
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
Copy the code
The createGetter code, which does a layer of interception in the getter Handler, when accessing reactiveFlags. RAW: The __v_RAW attribute returns target, the original target object, only if the caller the receiver points to is the proxy instance itself.
// createGetter
if(key === ReactiveFlags.RAW && receiver === (isReadonly ? ReadonlyMap: reactiveMap).get(target)) {reutrn target}Copy the code
If the intercepting handler (setter) is triggered by a proxy successor, the trigger is not triggered. This is the logic of limitation 1 above. (Note: Receiver refers to the actual source object that accesses the property and triggers the handler.)
// Demo, data push will trigger multiple set intercepts
let arr = ref(['a'.'b'])
arr.value.push('c')
// 'set' '2' 'c'
// 'set' 'length' 3
Copy the code
How do you avoid multiple triggers? The answer is limit 2, limit 3.
'set' '2' 'c'
This time,set
The trigger,target
There is no subscript2
To triggertrigger
.Limit 2
through'set' 'length' 3
This time,set
The trigger,target
There are propertieslength
.Limit 2
No, the new value is3
(length), the array has already performed the set operation, so the old value is also3
(length),Limit 3
It doesn’t pass either.
The trigger triggers the distribution of dependence.
trigger
The trigger is conditional.add
Method is todepsMap
中dep set
Add to localeffects
And then filter those that meet the criteriaeffect
For batch triggering to complete side effect execution. The input parameter to add, effectsToAdd, needs to take into account differences in data structures and marginal conditions. To summarize is to summarizedepsMap
Store dependent inSet
, collected according to the ruleseffects
In the.- The run method is used to batch execute effects added by the add method.
// The run method executes effect, introducing a Scheduler dispatcher
const run = (effect: ReactiveEffect) = > {
if (effect.options.scheduler) { // Effect is not executed immediately. Add effect to the scheduler to control the timing of effect execution, TODO
effect.options.scheduler(effect)
} else {
effect() // Effect executes immediately without special scheduling processing}}Copy the code
1.4 ref
Ref, used to convert a value to a responsive object. Unlike Reactive, where reactive can only accept input parameters of type Object, ref can accept primitive and reference types.
Const count = ref(0);
Ref accepts both primitive and reference data, which is a wrapper and extension based on the createReactive method.
- When a reference type data source is passed in,
ref
In theconvert
Will be calledcreateReactive
Method to transform data source responsively. - When a basic data type is passed in,
class RefImpl
When the value is accessed, dependency collection will be carried out through the track function. When the value is set, updates will also be distributed through track.
The ref is also named because ref returns a mutable reactive object as a reference to its internal value, a reactive. This object contains only one property named value.
const stateRef = ref(0)
stateRef++ // stateRef is declared const, which is a constant
stateRef.value++ StateRef is a reactive reference, and its value is a reactive object
Copy the code
1.5 other
If you’ve forgotten the previous one [5], let’s recap a little: When reactive(data) is implemented in setup, F11 enters the reactive initialization phase and returns a proxy reactive proxy through createReactiveObject. Get, set, etc.
In fact, the above code analysis is not necessary, but the track-trigger two deliberately designed routines have not been triggered… T_T Young people do not speak martial virtue…
Back to the starting point [1], setupRenderEffect immediately after installing the component instance activates the side effect in the render function, which is used for rendering. This brings us to the other core effect of responsiveness.
If you use effect directly in the setup function (computed, watch, and other apis are all based on Effect), effect in setup will be executed earlier than the effect for rendering that we’ll explain next. This can complicate the process of code analysis, so it is recommended to start with the simplest code to test.
例 句 : There are some side effects
An effect is a dependency in Vue3.0 responsiveness, also known as a side effect. He can establish a dependency between the incoming effect callback and the reactive data.
SetupRenderEffect mounts an update function to the component instance, which is an effect side effect and executes immediately after the effect is created.
2.1 createReactiveEffect
CreateReactiveEffect create effect
We can start by describing what effect should do:
- Execute function, callback function, execute can get corresponding side effects;
- Have dependent activation ability; An interception of a reactive object is ready, but the interception is never triggered, and the execution of an effect fires the getter to collect dependencies;
- Have the ability of self-renewal, logically form a closed loop; Collected dependencies cannot remain unchanged forever, and after side effects are consumed, fresh side effects should be collected again.
Let’s look at the execution of Effect (the blue module in the flow chart below)
- Cleanup, which cleans up the current effect each time an effect is executed. The collected dependencies are maintained as one
deps
(who does the record), the record holder depends on itselfdep set
. When an effect is in the process of execution, it removes itself in turn from the current effect’s DEPS reference. For example, leader-A and Leader-B both track A key requirement X that I made. As the executor of X, I know that the status of the requirement needs to be updated at any time. When X is completed, I will notify A and B, so that A and B will maintain the todo-list of their respective key projects. Delete the current X. There is another reason if reactive properties aredelete
The attribute collection DEP set should also be cleared. As long as it’s consumed, it’s discarded, and then as fn executes, it collects new dependencies, and that’s iteffect
The ability of self-renewal. - During the execution of the setup function
pauseTracking
To avoid dependency collection triggered by operations on state (such as state[key]),effect
Open track to resume collection when it is executed. When FN is executed, the current effect dependent collection is completed, and then track is restored to the last state - The effectStack is a global effect stack that prevents repeated pushes of activated effects and is used to perform scheduling
- Set the currently executing effect to a global activation effect that will be collected into the dependency repository during track
- Fn execution, the current flow is the render logic, which is the process of generating vNodes according to render and patch to the real DOM. This execution triggers getter intercepts for reactive objects, and collecting ActiveEffects completes dependency collection, which is the dependency activation capability.
- EffectStack Indicates the effect executed on the stack
Responsive workflow:
setupRenderEffect
As a side effect of activating the render function, mount one on the root component instanceupdate
Function, which is an immediate effect;effect
When executed, the current is clearedeffect
To re-collect dependencies;effect
Trigger getter interception of responsive objects, global data response relational repository collection dependencies;- Changes to responsive objects trigger updates, and effects are executed in batches
- Loop: 2 -> 3 -> 4 -> 2
3. Calculate attributesComputed
States that depend on other states return an immutable or writable reactive ref object.
computed
Pass in a getter function, like in demo() => state.counter * 2
, returns a ref object that cannot be manually modified by default.- Or pass in an include
get
和set
Function to create a calculated state that can be manually modified.
Computed return value is a ComputedRefImpl instance, so it is also a ref, which is a reference.
- In the constructor, pass the
getter
Wrap (the computation function passed in) into oneeffect
ComputedRefImpl
Instance in theget value
When intercepted, it is executed immediatelyeffect
, get the value of the calculation function; At the same timetrack
The currentComputedRefImpl
Example this data source and collect dependencies- If it is a writable computed property, the property triggers the computed property
set value
That will berun effect
So computed is Vue3.0’s clever practice of responsive systems, and the core is the same.
The _dirty property of the instance, which controls when the instance get value is recalculated to a new value, will only recalculate the new value of the property if it is true.
// computedRefImpl get value
get value() {
if (this._dirty) { // dirty Controls when to get new values. New calculations are triggered only when dirty is true
this._value = this.effect()
this._dirty = false
}
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
Copy the code
If _dirty => recalculate. How is it dirty? _dirty is set to dirty when effect is executed.
// computedRefImpl constructor declares the effect of the instance
this.effect = effect(getter, {
lazy: true.// For computed lazy updates, effect packaging does not perform immediately
scheduler: () = > {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')}}})Copy the code
The second section introduces the effect scheduler, the scheduler. The run method in trigger checks that a scheduler is present in effect.options and takes precedence over scheduler callbacks. Therefore, effect can use scheduler to perform some intermediate scheduling behaviors.
Let doubleCounter = computed(() => state.counter * 2) The flow for calculating side effects in attributes is scheduled as follows:
- Computed attributive
getter
The counter is updated, triggering the countertrigger
Distributed update - When (counter) collected
dep set
The run tocomputedRefImpl
的effect
Is executedscheduler
The callback will be_dirty
Set to true, trigger with no newValue parameter is activated, and another side effect (the one in this demo that eventually triggered the render side effect) is dispatched, with scheduling details involved - The above step has not been performed
computedRefImpl
的effect
的getter
Callback, currenteffect
Is waiting for an FN callback to be triggered. Intermediate state, waiting to trigger - In the calculated properties
scheduler
In thetrigger
The execution of render Effect will be triggered immediately. Fn callback of render Effect will be triggered when the calculation attribute (doubleCounter) is accessed during the process of regenerating VNode and patchcomputedRefImpl
的getter
The intermediate state in 3 is released because at this time_dirty
在scheduler
Set true at execution time to calculate the latest value of the computed property.
As FAR as I can see, it reduces the number of times you need to re-calculate the attributes. Effect uses scheduler to perform intermediate state scheduling behavior, which can be seen.
4. Watch
The Watch API is exactly equivalent to 2.x this.$watch (and the corresponding option in Watch). Watch needs to listen for specific data sources and perform side effects in callback functions. The default is lazy, meaning that callbacks are executed only when the source changes are being listened for.
In addition to the Watch API, a New watchEffect API has been added to Vue3.0.
watchEffect(() = > console.log(state.counter))
// Watch can be overloaded in a variety of ways, so we'll just use the common ones in 2.x.
const count = ref(0)
watch(
count,
(count, prevCount) = > {
/ *... * /})Copy the code
What is the difference between the two apis?
watchEffect
You don’t need to specify a listening attribute,watchEffect
The function passed in is executed immediately, dependencies are collected during execution, and the function is re-run when its dependencies change;watch
It is clear which state changes will trigger the listener restart side effects. The initialization phase does not need to perform collection dependencies immediately, it can be lazy side effects.watch
Can be in the callback functioncb
, access the values before and after the listening state change,watchEffect
There is no such callbackcb
export function watchEffect(effect: WatchEffect, options? : WatchOptionsBase) :WatchStopHandle {
return doWatch(effect, null, options)
}
export function watch<T = any.Immediate extends Readonly<boolean> = false> (
source: T | WatchSource<T>,
cb: any, options? : WatchOptions<Immediate>) :WatchStopHandle {
return doWatch(source as any, cb, options)
}
Copy the code
You can see that the core processing logic of both apis is doWatch
4.1 doWatch
The implementation of the two listening apis is based on doWatch(source, CB, options). The purpose of doWatch is:
- When the data source changes, the callback (CB) — watch API is performed
- 当
source
It’s a function, and it doesn’tcb
, the dependency is insource
The dependency needs to be collected first and rerun when its dependency changessource
— watchEffect API
The obvious need for a publish-subscribe model, like the implementation of computed attributes, is a clever exercise in a responsive system.
The overloading of the Watch API supports listening to a single data source, responsive single data source, and multiple data sources. The code needs to handle multiple scenarios, so we can look at the Implementation of watchEffect and understand the doWatch logic. Some of the execution timing scheduling logic of the Watch API, which relates to the scheduling implementation of Vue3.0, will be introduced separately.
getter = () = > {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onInvalidate]
)
}
Copy the code
watchEffect
The wrapped function can receive oneonInvalidate
Function to register callbacks for cleanup failures.cleanup
, is to perform a cleanup, which is triggered when the side effect is reexecuted and the listening is stoppedcallWithErrorHandling
Is the execution with the exception catching functionsource
function
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
})
Copy the code
The Scheduler in watchEffect has only a simple runner.
The second section introduces the effect scheduler, the scheduler. The run method in trigger checks that a scheduler is present in effect.options and executes the scheduler callback first, otherwise effect() is executed directly. WatchEffect plugs the execution of effect into the scheduler, which is equivalent to executing effect() directly. However, by adding some scheduling logic such as queuePreFlushCb to the scheduler, side effects are queued and executed after all components are updated, thus avoiding unnecessary repeated calls caused by multiple state changes in the same tick.
Finally, doWatch returns a function that clears the side effects.
The appendix
- Vue3 response. Drawio