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 theVue3 initializationIn the course of oursetupStatefulComponentAnd leave oneTODO: the pit Reactive, introduces: where the response is initialized.
  • We hit the break point atreactiveFunction calls here, take a look at the call stack, reviewVue3 initializationProcess, to connect with thisReactiveIn this paper.

Let’s take a look at our demosetupLet’s take this little piece of code and turn it overVue3.0Is 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.

  1. vue3.0The array method has been hacked, stored inarrayInstrumentationsHacks 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.

  1. trackCollect 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.

  1. Recursive processing, on-demand transformation,reactiveWhen 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.

  1. throughReflectThe 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,setThe trigger,targetThere is no subscript2To triggertrigger.Limit 2through
  • 'set' 'length' 3This time,setThe trigger,targetThere are propertieslength.Limit 2No, the new value is3(length), the array has already performed the set operation, so the old value is also3(length),Limit 3It doesn’t pass either.

The trigger triggers the distribution of dependence.

  1. triggerThe trigger is conditional.
  2. addMethod is todepsMapdep setAdd to localeffectsAnd then filter those that meet the criteriaeffectFor 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 summarizedepsMapStore dependent inSet, collected according to the ruleseffectsIn the.
  3. 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,refIn theconvertWill be calledcreateReactiveMethod to transform data source responsively.
  • When a basic data type is passed in,class RefImplWhen 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:

  1. Execute function, callback function, execute can get corresponding side effects;
  2. 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;
  3. 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 onedeps(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 aredeleteThe 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 iteffectThe ability of self-renewal.
  • During the execution of the setup functionpauseTrackingTo avoid dependency collection triggered by operations on state (such as state[key]),effectOpen 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:

  1. setupRenderEffectAs a side effect of activating the render function, mount one on the root component instanceupdateFunction, which is an immediate effect;
  2. effectWhen executed, the current is clearedeffectTo re-collect dependencies;
  3. effectTrigger getter interception of responsive objects, global data response relational repository collection dependencies;
  4. Changes to responsive objects trigger updates, and effects are executed in batches
  5. Loop: 2 -> 3 -> 4 -> 2

3. Calculate attributesComputed

States that depend on other states return an immutable or writable reactive ref object.

  1. computedPass in a getter function, like in demo() => state.counter * 2, returns a ref object that cannot be manually modified by default.
  2. Or pass in an includegetsetFunction 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 thegetterWrap (the computation function passed in) into oneeffect
  • ComputedRefImplInstance in theget valueWhen intercepted, it is executed immediatelyeffect, get the value of the calculation function; At the same timetrackThe currentComputedRefImplExample this data source and collect dependencies
  • If it is a writable computed property, the property triggers the computed propertyset valueThat 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:

  1. Computed attributivegetterThe counter is updated, triggering the countertriggerDistributed update
  2. When (counter) collecteddep setThe run tocomputedRefImpleffectIs executedschedulerThe callback will be_dirtySet 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
  3. The above step has not been performedcomputedRefImpleffectgetterCallback, currenteffectIs waiting for an FN callback to be triggered. Intermediate state, waiting to trigger
  4. In the calculated propertiesschedulerIn thetriggerThe 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 patchcomputedRefImplgetterThe intermediate state in 3 is released because at this time_dirtyschedulerSet 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?

  1. watchEffectYou don’t need to specify a listening attribute,watchEffectThe function passed in is executed immediately, dependencies are collected during execution, and the function is re-run when its dependencies change;
  2. watchIt 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.
  3. watchCan be in the callback functioncb, access the values before and after the listening state change,watchEffectThere 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
  • sourceIt’s a function, and it doesn’tcb, the dependency is insourceThe 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
  1. watchEffectThe wrapped function can receive oneonInvalidateFunction 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 stopped
  2. callWithErrorHandlingIs the execution with the exception catching functionsourcefunction
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