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