Vue3 Reactivity

This chapter introduces another very important module in Vue, reactive. Introduces the basic principles (including diagram) simple implementation and how to read the source code.

Thanks Vue Mastery very good course, can be reproduced, but please declare source link: article source link justin3go.com(some latex formulas cannot render on some platforms can be viewed on this website)

Reactivity

role

  • Learning the responsivity principle of Vue3;
  • Improve debugging ability;
  • Use the reactive module to perform some operations;
  • Contribute to Vue

How does Vue know to update these templates?

Vue knows how to update these templates when the price changes.

JavaScript doesn’t update!

Implement a responsive principle yourself

Basic idea: When the price or quantity is updated, let it run again;

// effct recalculates total; Let effect = () => {total = price * quantity} // shorten the above intermediate codeCopy the code
Let dep = new Set()Copy the code

  • The track() function adds this data;
  • The trigger() function < trigger function > iterates through every effect we have stored and runs them;

How is it stored so that each property has its own dependency

Typically, our objects will have multiple properties, each requiring its own DEP (dependency), or effect Set;

  • A DEP is an effect Set (Set) that should be run again when the value changes;

  • To store these DEPs so that we can find them later, we need to create a depsMap, which is a map of the DEP objects for each attribute;
  • Using the property names of the object as keys, such as quantity and price, the value is a DEP (Effects set).
  • Code implementation:
Const depsMap = new Map() function track(key) {let dep = depsmap.get (key); // Find the attribute's dependency if (! Dep){depmap.set (key, (dep = new set()))} dep. Add (effect)Copy the code
Function trigger(key) {let dep = depmap.get (key) if (dep) { ForEach (effect => {effect()})}}Copy the code
// main let product = { price: 5, quantity: 2}; let total = 0; let effect = () => { total = product.price * product.quantity; } // store effect track('quantity') effect()Copy the code
  • Results:

What if we have multiple responsive objects?

Here’s what came before:

DepsMap stores each attribute, and at the next level, we need to store each object, and each object contains a different attribute, so on top of that we have a table (Map) to store each object;

The figure in Vue3 is called targetMap, and it should be noted that the data structure is WeakMap<, which means that when the key value is empty, it will be cleared by garbage collection mechanism, that is, when the responsive object disappears, the related dependencies stored in it will be automatically cleared. >

const targetMap = new WeakMap(); // Store dependencies for each responsive object // Then the track() function needs to get the depsMap of the targetMap function track(target, key) { let depsMap = targetMap.get(target); // Target is the name of the responsive object, and key is the name of the attribute in the object. Targetmap. set(target, (depsMap = new Map())}; let dep = depsMap.get(key); If (! Dep){// Create a new depmap.set (key, (dep = new set()))}; dep.add(effect); }Copy the code
Function trigger(target, key){const depsMap = targetmap. get(target) // Check whether the object has dependent attributes if(! DepsMap){return} let dep = depsMap. Get (key) Run each effect if (dep){dep.foreach (effect => {effect()})}}Copy the code
// main
let product = {price: 5, quantity: 2}
let total = 0
let effect = () => {
    total = product.price * product.quantity
}
track(product, "quantity")
effect()
Copy the code

Run:

conclusion

The response is recalculated each time the value is updated. However, due to some dependencies and attributes of certain objects, other variables change, so some data structures are needed to record these changes, resulting in the following tables (Map, Set).

However, we currently have no way to restart our effect automatically, which will be covered later;

Agency and reflection

The introduction of

In the previous part, we used track() and trigger() to explicitly construct a responsive engine. In this part, we hope that it can automatically track and trigger.

Requirements:

  • Accessing product properties or using the get method is when we want to call track to save effect
  • The product property changes or uses the set method, where we want to call trigger to run those saved effects

Solution:

  • In Vue2, object.defineProperty () in ES5 is used to intercept get or set.
  • Vue3 uses ES6 proxies and reflection to achieve the same effect;

Proxy and reflection fundamentals

There are usually three ways to get an attribute in an object:

  • let product = {price: 5, quantity: 2};
    product.quantity
    Copy the code
  • product[quantity]
    Copy the code
  • Reflect the get (product, "quantity")Copy the code

As shown in the following figure, when printing, the order of invocation: it calls the proxy first, which is simply an object delegate, where the second argument to the proxy is a handler function, in which we can print a string, as shown in the following figure.

This is a direct change to the default behavior of GET. Usually we just need to add some code to the original behavior, and then we need reflection to call the original behavior.

And then the log here will call the get method, and the GET method is in the proxy, so you can’t go up any further, and the arrow stops in the picture below;

To use reflection in the proxy we need to add an additional parameter (receiver passed to our Relect call), which ensures that this pointer points correctly when our object has a value or function inherited from another object;

Then, we intercept set:

Run the call:

encapsulation

Output:

Add Track and Trigger

Then we go back to the original code, we need to call track and trigger:

Remember:

  • Function of track: Add the effect corresponding to the attribute value (key) into the set;
  • For example, I define a responsive object, which contains the unit price and quantity attributes, and then I define a variable total=product.price+product.quantity; Here get is called, that is, the change of price and quantity will affect the variable total. Track is called in GET to save the calculation code into the collection.
  • Trigger is used to rerun all effect functions.
  • Trigger is added to SET: when the value changes each time, the corresponding effect function is run, for example, when the unit price changes (set is executed), the total price will be recalculated by the effect function.

Overall operation process

ActiveEffect&Ref

Track in the previous section will iterate over the property values in target< reactive Object > and various dependencies to ensure that the current effect is recorded, but this is not what we want. We should only call the trace function in effect < that is, only the properties of the responsive object used in effect will be saved and recorded, but not in effect, such as logging will not be saved, and track() will not be called to track, which is what we need to achieve >;

First we introduce an activeEffect variable, which is currently running effect(which is Vue3’s solution to this problem).

let activeEffect = null
Copy the code
Function effect(eff){activeEffect = eff activeEffect() activeEffect = null} What is the effect of saving multiple activeEffect variables? < below will use this variable to judge something > / / with the following if to solve the problem of the first < avoiding traversal >Copy the code

We then call this function, which means we don’t need to use the following effect() function because it will be called in the activeEffect() step of the above function:

Now we need to update the track function to use the new activeEffect:

  • First, we only want to run this code when we have activeEffect:
  • When we add a dependency, we add an activeEffect

Testing (adding more variables or objects)

When product. Price = 10, it is clear that both effects are running;

Running results:

We’ll find that it stops working when we use salePrice to calculate totals:

Because in this case, when the salePrice is determined, the total needs to be recalculated, which is not possible because salePrice is not responsive < change in salePrice does not cause total to recalculate >;

How to achieve:

We’ll see that this is a good place to use Ref;

let salePrice = ref(0)
Copy the code

A mutable Ref object that takes a value and returns a response. The Ref object has only a “. Value “property that points to an internal value,

Now let’s think about how to define Ref()

1. You can simply use reactive to set the value of the key to its initial value:

function ref(intialValue) {
    return reactive({value: intialValue})
}
Copy the code

2. Solution in <Vue3 > Computed properties in JavaScript:

Object accessors are used here: Object accessors are functions that get or set values

Next use object accessors to define Ref:

function ref(raw) { const r = { get value(){ track(r, Return raw} set value(newVal){raw = newVal trigger(r, 'value') // Call trigger function} return r}}Copy the code

Here’s a test to see if it works:

When the quantity is updated, the total changes; When price is updated, total changes with salePrice.

Compute&Vue3-Source

Maybe our sales price and total price here should use calculated attributes

How should we define the method of calculation?

Calculations of properties or values are very similar to reactive and Ref values — dependent properties change, resulting in changes…

  • Create a reactive reference called result;
  • Run the getter in effect because we need to listen for the response value and assign it to result.value;
  • Returns the result

  • Here’s the actual code:
  • function computed(getter) {
        let result = ref()
        effect(() => (result.value = getter()))
        return result
    }
    Copy the code
  • The test results are as follows:

Compare the response formula of Vue2:

In Vue2, after creating a reactive object, there is no way to add new reactive properties, which are obviously more powerful.

Because the name is not reactive, in Vue2, get and set hooks are added to each attribute, so there are other things we need to do when adding new attributes:

However, using a proxy now means we can add new properties and they will automatically become reactive

View source code:

Q&A

Problem a

In Vue2, we call Depend to save the code and notify to run the saved code. But in Vue3, we call track and trigger. Why?

  • Basically, they’re still doing the same thing; An even bigger difference is that when you name them depend and notify are verbs related to the owner (Dep, a dependent instance), so to speak a dependent instance is being depended on or it is informing its subscribers (subcribers)
  • In Vue3, we made a slight change to the implementation of dependencies. Technically, there are no Dep classes anymore. The logic of deopend and notify is now separated into two separate functions (track and trigger). They are more like tracking something rather than something dependent (A.b –> B.Call (a))

Question 2

In Vue2, we have a separate Dep class; At Vue3 we had only one Set, why did we make this change?

  • The Dep class in Vue2 makes it easier to think that a dependency has a certain behavior as an object
  • In Vue3, however, we separated Depend and notify into Track and trigger, and now they are two independent functions, so there is only one Set of classes left. In this case, it is meaningless to use another class to encapsulate the Set class itself, so we can declare it directly instead of letting an object do it.
  • For performance reasons, this class is really not needed

Question 3

How did you arrive at this solution (the reactive approach in Vue3)?

  • When you use getters and setters in ES5, as you traverse the key on an object (forEach), there’s naturally a little closure that stores the associated Dep for the property;
  • In Vue3, after using Proxy, the Proxy handler accepts the target and key directly, and you don’t really get a closure to store the associated dependencies for each property;
  • Given a target object and a key on that object, how can we always find the corresponding dependency instances? The only way is to divide them into two nested graphs of different levels.
  • The name targetMap comes from the Proxy

Problem four

Refs can be defined by returning a reactive, as opposed to using object accessors in the Vue3 source code.

  • A Ref is defined to expose only one property (the value itself), but if you use Reactive, you technically attach new properties to it, which defeats the purpose of a Ref;
  • Ref can only wrap an internal value and should not be treated as a generic reactive object;
  • IsRef checks that the returned REF object actually has something special that lets us know that it is actually a REF and not a Reactive, which is necessary in many cases;
  • Performance issues, as do more responsive, is more than we do in our Ref, when you’re trying to create a responsive object, we need to check whether there have been corresponding conforms to the copy of the response type, check if it is a read-only copy, when you create the response object, there will be a lot of extra work to do, Using an object literal to create a REF here is more performance efficient

Problem five

What are the other benefits of using reflection and proxies to add attributes

  • When we use proxies, the so-called reactive transformation becomes lazy loading;
  • In Vue2, when we do the conversion, we have to do it as quickly as possible, because when you pass the object to Vue2’s response, we have to go through all the keys and convert them on the spot. When they are accessed later, they have been converted;
  • But with Vue3, when you call Reactive, for an object, all we do is return a proxy object and nest the object only when you need to transform it (when you need to access it), which is kind of lazy loading;
  • This way, if your app has a huge list of objects, but for pagination, you only render the first 10 of the page, that is, only the first 10 have to be transformed in a responsive manner, which saves a lot of time starting the application;

The source code

// packages\reactivity\src\baseHandlers.ts
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false.true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true.true)

const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)
Copy the code

createGetter

// packages\reactivity\src\baseHandlers.ts
function createGetter(isReadonly = false, shallow = false) {
    // There are isReadonly and shallow versions
    // isReadonly allows you to create read-only objects that can be read and traced but cannot be changed
    Shallow means that as a nested property when an object is put into another object, it does not attempt to convert it to reactive
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)
	
    / / (1)
    if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if(! isReadonly) { track(target, TrackOpTypes.GET, key)// The actual code to execute
    }

    if (shallow) {
      return res
    }
	// If you nested a Ref in a reactive object, it will be unwrapped automatically when you access it.
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
    }
	// Make sure to convert it only if it is an object
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}
Copy the code
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  // instrument identity-sensitive Array methods to account for possible reactive
  // values; (['includes'.'indexOf'.'lastIndexOf'] as const).forEach(key= > {
    instrumentations[key] = function (this: unknown[], ... args: unknown[]) {
      const arr = toRaw(this) as any
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, TrackOpTypes.GET, i + ' ')}// we run the method using the original args first (which may be reactive)
      constres = arr[key](... args)if (res === -1 || res === false) {
        // if that didn't work, run it again using raw values.
        returnarr[key](... args.map(toRaw)) }else {
        return res
      }
    }
  })
  // instrument length-altering mutation methods to avoid length being tracked
  // which leads to infinite loops in some cases (#2137); (['push'.'pop'.'shift'.'unshift'.'splice'] as const).forEach(key= > {
    instrumentations[key] = function (this: unknown[], ... args: unknown[]) {
      pauseTracking()
      const res = (toRaw(this) as any)[key].apply(this, args)
      resetTracking()
      return res
    }
  })
  return instrumentations
}
Copy the code

We have an array (reactive array), and when we access something nested inside, we get a reactive version of the original data

// A marginal case
const obj = {}
const arr = reactive([obj])
const reactiveObj = arr[0]
// Compare objects with reactive objects
obj === reactiveObj  // is false
Copy the code

The problem with this is that if you use indexOf to find obj< this will cause problems if you don’t do array detector (1) >

const obj = {}
const arr = reactive([obj])
arr.indexOf(obj)  // -1
Copy the code

createSetter

// packages\reactivity\src\baseHandlers.ts
function createSetter(shallow = false) {
  return function set(target: object, key: string | symbol, value: unknown, receiver: object) :boolean {
    let oldValue = (target as any)[key]
    // Users cannot operate on read-only properties
    // isRef(oldValue) && ! IsRef (value) checks whether an attribute is being set
    if(isReadonly(oldValue) && isRef(oldValue) && ! isRef(value)) {return false
    }
    if(! shallow && ! isReadonly(value)) {if(! isShallow(value)) { value = toRaw(value) oldValue = toRaw(oldValue) }if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true}}else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, 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)) {
        // Add if there is no key
      if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
Copy the code

Use delete to remove the delete key:

function deleteProperty(target: object, key: string | symbol) :boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
Copy the code

track

// packages\reactivity\src\effect.ts
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if(! isTracking()) {// Some internal flags, in some cases should not be traced (1)
    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())) }// This is that effect
  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined
	/ / (2)
  trackEffects(dep, eventInfo)
}
Copy the code

(1)

export function isTracking() {
  returnshouldTrack && activeEffect ! = =undefined
}
ActiveEffect means that track will be called no matter what. If the reactive object has just been accessed without any currently running effects, it will be called anyway
Copy the code

(2)

export function trackEffects(dep: Dep, debuggerEventExtraInfo? : DebuggerEventExtraInfo) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if(! newTracked(dep)) { dep.n |= trackOpBit// set newly trackedshouldTrack = ! wasTracked(dep) } }else {
    // Full cleanup mode.shouldTrack = ! dep.has(activeEffect!) }if (shouldTrack) {
    dep.add(activeEffect!)
    // This is a two-way relationship between deP and effect is many-to-many
            // We need to keep track of it all and clean it upactiveEffect! .deps.push(dep)if(__DEV__ && activeEffect! .onTrack) { activeEffect! .onTrack(Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}
Copy the code

trigger

When cleaning up a collection, all effects associated with it must be triggered

// 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
    return
  }

  let deps: (Dep | undefined=) [] []if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) = > {
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if(key ! = =void 0) {
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if(! isArray(target)) { deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))}break
      case TriggerOpTypes.DELETE:
        if(! isArray(target)) { deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break}}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))
    }
  }
}
Copy the code