introduction
In the previous chapter, we analyzed what happened to createApp? We should have continued to analyze the mount method downward, but many places later involve responsive APIs, that is, reactive APIs, so we must take out this chapter separately for a separate explanation. The main analysis of this chapter:
Part 1: Simple Reactivity
- Responsiveness is mainly used to achieve the following effects
//Set responsive objectconst proxy = reactive({a:1}) //When there is a change, call the callback function in effecteffect(()=>{ () }) ++
Let's design a solution to achieve this effect
- There is a callback function in effect. When the callback function is executed for the first time, we need to listen to what responsive objects are inside the function.
- How to make the internal responsiveness correlate with the currently executed function? We can set a global variable activeEffect when we are about to execute this callback function, so that the side effect function that is about to be executed is activeEffect, and then collect this function in the responsive get. After executing this function, set activeEffect to null immediately, so that it will not affect the execution of other collection dependencies.
- When this proxy changes, it immediately finds the collected dependency trigger, which achieves this effect.
- Think about what kind of structure this is? First of all, the object and the object are constructed correspond to a dependency, and there may be multiple dependencies. Considering that if the object loses reference, then the dependency will not be called. We use the weakMap structure, an object corresponds to a depsMap, and the object contains multiple keys, an object plus a key corresponds to a dep dependency set, an object and a key may be used multiple times in different effects, and there may be multiple dependencies, so the dep type is Set, which is the following structure
reactiveMap = { [object Object]:{ [key]:new Set() } }
After understanding the entire design process, we started writing code:
(1). Implement reactive and effect
1. Set global variables when the effect function is about to start executing
let activeEffect = null; const reactiveMap = new WeakMap(); function effect(fn) { const reactEffect = new ReactiveEffect(fn); ();//Execute side effect function immediately for the first time} //Store side effectsclass ReactiveEffect { constructor(fn, scheduler) { = fn; = scheduler;//Scheduler } run() { try { //Modify activeEffect before executing activeEffect = this; return (); } finally { //Set activeEffect to null after execution activeEffect = null; } } }
2. Create reactive function
function reactive(obj) { //Collection of dependencies when obtaining values const getter = function (object, key, receiver) { const res = (object, key, receiver); //Get the real value track(object, key, res); if (typeof res === "object") { return reactive(res); } return res; }; //Trigger dependency when setting value const setter = function (object, key, value, receiver) { const res = (object, key, value, receiver); trigger(object, key, value, res); return res; }; const mutations = { get: getter, set: setter, }; const proxy = new Proxy(obj, mutations); return proxy; }
3. Implement track and trigger functions
function track(object, key, oldValue) { //First check whether there is a depsMap of this object before //If there is no representation, it is the first time that I collect it to create a new map let depsMap = (object); if (!depsMap) { (object, (depsMap = new Map())); } //If this key is collected for the first time, create a new dep dependency let dep = (key); if (!dep) { (key, (dep = new Set())); } Found thistargetandkeySide effects are collected after corresponding dependencies trackEffects(dep); } function trackEffects(dep) { //Because the object set is responsive, just //Responsive object changes will be collected, but only // ActiveEffect only has a value when the effect is executed //Only, dependencies can be collected and dep uses sets to prevent //Repeat the same dependency if (activeEffect) { (activeEffect); } } //Trigger the dependency function when modifying the valuefunction trigger(object, key, newVal, oldVal) { const depsMap = (object); const dep = (key);//Find the dep corresponding to the target and key //Execute dependent functions if ( > 0) { for (const effect of dep) { if () { (); } else (); } } }
This is the core logic of reactivity. Do you think it is very simple? Currently we are proxying an object, what if it is a value when we proxy? Then we need to use ref. Let’s write a minimalist version of ref implementation!
(2). Implement ref
- We can use the method of wrapping the value into an object, and use the interceptor that comes with the class to collect dependencies when getting, so collecting dependencies requires target and key. Obviously, target is the RefImpl instance, and key is the value. Also, when the dependency is triggered at set, ref is implemented
function ref(value) { return createRef(value); } function createRef(value) { return new RefImpl(value); } class RefImpl { constructor(value) { this.__v_isRef = true; this._value = value; } get value() { track(this, "value"); return this._value; } set value(value) { this._value = value; trigger(this, "value", value, this._value); return true; } }
(3). Implement computed
Speaking of computed, how did he achieve it? Let's talk about its requirements first. Computed accepts a getter and must have a return value. When the responsive formula collected internally changes, we will also change accordingly when reading, and the object returned by computed is also responsive. For example:
//Set responsiveconst proxy = reactive({ a: 1, b: { a: 1 } }); //Set calculation propertiesconst comp = computed(() => { return + 1; }); effect(() => { (); }); //When there is a change, the value of reading comp will also change, and because comp is responsive//It is collected in effect, so when changes occur, it will also cause the function in effect to be executed++;
Let's take a look at his implementation
Here we must say a scheduler. ReactiveEffect accepts two parameters. If there is a second parameter, then the run method will not be called but the scheduler method.
Therefore, the implementation principle of computed is to create ComputedRefImpl when executing the computed function, and the ReactiveEffet will be automatically created in the constructor. At this time, a schduler will be passed. That is to say, this effect will not call the run method but the schduler method. We only need to set dirty to true in the shcduler method to indicate that the value has been modified, and then schedule it, and then use the collected dependencies. Therefore, there are actually two places for the responsiveness here. The first place is that there is a responsiveness inside computed, and the second is that computed also needs to collect dependencies. When the responsiveness inside computed changes, it will cause this._effect.scheduler to be executed, then dirty will be set to true. When it is in other effects, the track collects dependencies. Therefore, when the responsiveness inside computed changes, the effect collected during get will be triggered.
class ComputedRefImpl { constructor(getter) { //Scheduler this._effect = new ReactiveEffect(getter, () => { if (!this._dirty) { this._dirty = true; //Modify the value heard in getter //Create updates within dep for (const effect of ) { if () { (); } else (); } } }); = new Set(); //rely this._dirty = true; //Is it necessary to update this._value = null; } get value() { trackEffects(); //Collect dependencies if (this._dirty) { this._dirty = false; this._value = this._effect.run(); } return this._value; } }
Finally, let's try the effect
const proxy = reactive({ a: 1, b: { a: 1 } }); const comp = computed(() => { return + 1; }); const proxyRef = ref(100); effect(() => { (); (); }); effect(() => { (); }); ++; ++; ++; //log:1 2 100 1 3 2 3 101
OK! After reading the recommended version of reactivity, I believe you have basically understood reactivity. Let’s start analyzing the source code!
Part 2: In-depth analysis of responsive agents for objects and arrays
- We redirect the most commonly used API and Reactive package to start analyzing. Because the factory function is used, the corresponding shallow, readonly, and shallowReadonly will also be analyzed.
- Let's take a look at the reactive function first
//Deep Agentexport function reactive(target) { //If the proxy is readonly returns the target that has been proxyed by readonly if (isReadonly(target)) { return target; } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap ); } //Only proxy the first layerexport function shallowReactive(target) { return createReactiveObject( target, false, shallowReactiveHandlers, shallowCollectionHandlers, shallowReactiveMap ); } //Proxy read-only attributeexport function readonly(target) { return createReactiveObject( target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap ); } //Proxy only read-only first layerexport function shallowReadonly(target) { return createReactiveObject( target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap ); }
We found that these four APIs essentially call createReactiveObject, but the parameters they pass are different, and the handling of different proxy handlers is different. Among them, for proxying map sets, etc., it is unnecessary for us to proxy this object again, and reactiveMap is required to cache it. The object that has been proxyed can read the cache.
Next, let's go deep into createReactiveObject and take a look at the source code first.
export function createReactiveObject( target, isReadonly, baseHandlers, collectionHandlers, proxyMap ) { //Not able to proxy non-object if (!isObject(target)) { { (`value cannot be made reactive: ${String(target)}`); } return target; } // Objects that have been proxyed do not need to be reproduced in secondary if (target[RAW] && !(isReadonly && target[IS_REACTIVE])) { return target; } //Prevent duplicate proxy const existingProxy = (target); if (existingProxy) { return existingProxy; } //Get the type of the currently proxy object //To be 0 means that the proxy object is an unexpandable object //or the current object contains the __v_skip attribute //To be 1 means Array, the Object type is processed by baseHandlers //To 2 means map set weakMap weakSet handled with collectionHandlers const targetType = getTargetType(target); //Not proxying Return to the original object if (targetType === 0) { (`current target:${target} can not been proxy!`); return target; } //Product const proxy = new Proxy( target, //Judge the type of the current proxy object. If it is an array object, use baseHandlers. //If it is map set weakMap weakSet uses collectionHandlers targetType === 2 ? collectionHandlers : baseHandlers ); (target, proxy); //Return the object that was successful in the proxy return proxy; }
- This function is relatively simple. First of all, in the first case, calling reactive(target) and then calling reactive(target) again will return the same proxy object, because the reactiveMap cache is established internally.
- The second case is to get proxy = reactive(target) and then proxy reactive(proxy). In order to prevent secondary proxying, you will eventually choose to return proxy.
- Of course, it will also be judged that for those who are not objects, they cannot be proxyed.
- Later, the current proxy type was judged through targetType, and different proxy methods were used for different types. Let's take a look at the getTargetType function.
//If the object has __v_skip or the object cannot be extended, it cannot be proxyed// Then judge which function is needed to proxy according to the typeexport function getTargetType(value) { return value[SKIP] || !(value) ? 0 : targetTypeMap(toRawType(value)); } //Judge the intercepted type if it is an object array//If it is set map weakMap weakSet returns 2export function targetTypeMap(rawType) { switch (rawType) { case "Object": case "Array": return 1; case "Map": case "Set": case "WeakMap": case "WeakSet": return 2; default: return 0; } } //Intercept typeexport const toRawType = (value) => { //Intercept "Object" in [object Object] return (value).slice(8, -1); };
In this section we only discuss the proxy for object and array types, so we skip the implementation of collectionHandlers. Now let's take a look at baseHandlers. BaseHandlers are obviously different handlers passed according to shallow readonly, which contains:
- mutableHandlers
- shallowReadonlyHandlers
- readonlyHandlers
- shallowReactiveHandlers Let's see how he created these four handlers!
//reactive proxy handlers//This is the second parameter in new Proxy(), which can intercept get//set deleteProperty has ownKeys, etc.const mutableHandlers = { get, set, deleteProperty, has, ownKeys, }; //Processing readonly proxy handlerconst readonlyHandlers = { get: readonlyGet, //No set value is required for readonly handlers //Print warning, but do not modify the value set(target, key) { { warn( `Set operation on key "${String(key)}" failed: target is readonly.`, target ); } return true; }, //For read-only attributes, the value cannot be deleted deleteProperty(target, key) { { warn( `Delete operation on key "${String(key)}" failed: target is readonly.`, target ); } return true; }, }; //Processing proxy handlers that only proxy layer 1const shallowReactiveHandlers = ({}, mutableHandlers, { get: shallowGet, set: shallowSet, }); //Processing proxy handlers that only do read-only proxying for the first layerconst shallowReadonlyHandlers = ({}, readonlyHandlers, { get: shallowReadonlyGet, }); // Here is the Object's assign method// =
Obviously, several proxy functions appear in the above code, namely getter setter deleteProperty ownKeys has, and we will analyze each one next.
(1).getter in handlers
- We found that for getters, there are shallowGet, readonlyGet, shallowReadonlyGet and get, and we see how to get these methods.
const get = createGetter(); const shallowGet = createGetter(false, true); const readonlyGet = createGetter(true, false); const shallowReadonlyGet = createGetter(true, true);
They all call the createGetter method, which is a factory function. By passing isReadonly isShallow, we can determine which type of getter it is, and then create different gets. So next we naturally need to analyze the createGetter function.
//Create the factory function of getter, by whether it is read-only and//Whether to only proxy the first layer to create different getter functionsexport function createGetter(isReadonly = false, shallow = false) { //Pass the get function into Proxy //For example const obj = {a:2} // const proxy = new Proxy(obj,{ // get(target,key,receiver){ // When accessing obj, you will first enter this function // The return value will be used as the obtained value // } // }) return function get(target, key, receiver) { //1. Processing of isReadonly isShallow and other methods //The following previous judgments are all for the purpose of passing some key judgments //Is the current object being proxyed or is it read-only //Is it only the first layer agent? //Suppose our agent is currently reactive type //If we access __v_isReactive then the return value should be true // Similarly, accessing the readonly type returns false //Therefore, here is reversed if (key === IS_REACTIVE) { return !isReadonly; } //Add to __v_isReadonly return isReadonly true value else if (key === IS_READONLY) { return isReadonly; } //Add to __v_isShallow and return the true value of shallow else if (key === IS_SHALLOW) { return shallow; } //When accessing __v_raw, according to the current readonly and shallow properties //Access different map tables and obtain the object before the proxy through the map table else if ( key === RAW && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target; } //Judge whether the current target is an array const targetIsArray = isArray(target); //If the push pop shift unshift splice is called includes indexOf lastIndexOf //Intercept this method if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return (arrayInstrumentations, key, receiver); } //Get the real value of the access const res = (target, key, receiver); //Judge whether the currently accessed key is a built-in Symbol property or whether it is a key that does not require track //For example, __proto__ , __v_isRef , __isVue will be returned directly if these properties are if (isSymbol(key) ? (key) : isNonTrackableKeys(key)) { return res; } //If it is not a read-only attribute, start collecting dependencies. Read-only attributes do not need to collect dependencies. if (!isReadonly) { track(target, , key); } //Only, you need to proxy and return it without proxying. if (shallow) { return res; } //If the value accessed is of type ref, return //A numeric properties of the array are accessed and then res is returned if (isRef(res)) { return targetIsArray && isIntegerKey(key) ? res : ; } //If the result is still the object continues to be in-depth proxy if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res); } return res; }; }
- First of all, for objects that have been proxyed, you can judge whether it is reactive, shallow, and readonly by judging key=__v_isReactive, __v_isShallow, __v_isReadonly. Of course, this is also the basis for the implementation of APIs such as isReactive and isReadonly.
- We do not need to collect dependencies such as [] for access to certain special attributes later.
- If it is not a read-only proxy, you need to collect dependencies to facilitate subsequent effect calls.
- If the value accessed is still an object, we still need to perform in-depth proxy.
isNonTrackableKeys function, builtInSymbols, and how to deal with it if the array calls the push pop include method?
//Posted the source code here, read carefully if you are interested, and will not explain it.const isNonTrackableKeys = makeMap(`__proto__,__v_isRef,__isVue`); function makeMap(str, expectsLowerCase) { const map = (null); //Create an empty object const list = (","); //["__proto__","__isVUE__"] for (let i = 0; i < ; i++) { map[list[i]] = true; //{"__proto__":true,"__isVUE__":true} } //Return a function to determine whether it is a certain value divided by the passed str // You can specify whether the delimited value needs to be converted to lowercase through expectsLowerCase return expectsLowerCase ? (val) => !!map[()] : (val) => !!map[val]; } //All attribute values of Symbolexport const builtInSymbols = new Set( //First get all Symbol keys (Symbol) //Filter out arguments and callers .filter((key) => key !== "arguments" && key !== "caller") //Get all Symbol values .map((key) => Symbol[key]) //Filter out values that are not symbol .filter() );
- buildInSymbols is all built-in properties keys of Symbol, such as etc.
- Let’s take a look at how to handle the call of special methods in arrays.
//The object of the current proxy is an array, and one of the 8 methods such as pop has been accessed.if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { //Product return (arrayInstrumentations, key, receiver); } const arrayInstrumentations = createArrayInstrumentations(); function createArrayInstrumentations() { const instrumentations = {}; //Methods to intercept arrays () ["includes", "indexOf", "lastIndexOf"].forEach((key) => { instrumentations[key] = function (...args) { //This here points to the array that calls the current method const arr = toRaw(this); //Colle all elements in the current array to depend on for (let i = 0, l = ; i < l; i++) { track(arr, , i + ""); } //Execute the function const res = arr[key](...args); if (res === -1 || res === false) { return arr[key](...(toRaw)); } else { return res; } }; }); // If you use these methods to cancel the collection dependency ["push", "pop", "shift", "unshift", "splice"].forEach((key) => { instrumentations[key] = function (...args) { //Stop collecting dependencies and change shouldTrack to false pauseTracking(); // Here toRaw is to prevent the secondary execution of getters and execute the corresponding method of the array. const res = toRaw(this)[key].apply(this, args); //Recollect dependencies and change shouldTrack to true resetTracking(); return res; }; }); return instrumentations; } //Off trackingexport function pauseTracking() { (shouldTrack); shouldTrack = false; } //Reset Trackingexport function resetTracking() { //Get the previous shouldTrack value const last = (); //If there is no value in trackStack, shouldTrack is set to true shouldTrack = last === undefined ? true : last; }
- First of all, include, indexOf, and lastIndexOf will traverse all elements in the array and will have a obtain operation, that is, all elements of the array may be accessed and executed, so all elements in the entire array must be tracked.
- For five methods such as pop, dependency collection is confusing. For example, if I perform shift operations, the underlying layer needs to move elements, which obviously leads to multiple triggers of getters and setters, so we must stop collecting dependencies.
OK, let's analyze the track function and see how dependencies are collected.
export function track(target, type, key) { //When the effect method is called, the activeEffect will be assigned a value to if (shouldTrack && activeEffect) { let depsMap = (target); if (!depsMap) { (target, (depsMap = new Map())); } let dep = (key); if (!dep) { (key, (dep = createDep())); } //Transfer information of the current effect of the life cycle hook const eventInfo = { effect: activeEffect, target, type, key }; trackEffects(dep, eventInfo); } }
- I believe everyone is already quite familiar with this method! It is the same as the simple version of reactivity we wrote, which is to obtain dependencies through target and key, and create them if they are not.
- So when is activeEffect assigned? I believe that everyone already knows in the simple version of reactivity, which means that the value is assigned before calling effect, and becomes null after the call is completed. However, the implementation of the source code is more complex and the considerations are more comprehensive.
export class ReactiveEffect { constructor(fn, scheduler = null, scope) { = fn; //Side Effects Function //Scheduler (if there is a caller, it will not execute the run method but the scheduler) = scheduler; = true; /** * The current side effects are depended on by those variables * For example: * effect(()=>{ * () * }) * effect(()=>{ * () * }) * * Each effect callback function will generate a ReactiveEffect instance * If the first effect is read, it will be collected and dependencies will be collected. * For the first ReactiveEffect instance, there is * That is, the dep pointed to by the target key. This dep is a collection, which represents * target key corresponding dep */ = []; = undefined; //TODO recordEffectScope recordEffectScope(this, scope); } //Start execution run() { if (!) { return (); } let parent = activeEffect; let lastShouldTrack = shouldTrack; while (parent) { if (parent === this) { return; } parent = ; } try { // There may be nested effects, when there is effect in the effect callback function when there is effect //The current activeEffect is equivalent to the parent effect of the newly created effect /* For example: effect(()=>{ Now point to external effect () effect(()=>{ When activeEffect points to internal effect () }) Now you need to restore activeEffect to external effect () }) Of course, the corresponding parent should also be changed, this is the role of try finally */ = activeEffect; //Let the current activeEffect be the current effect instance activeEffect = this; shouldTrack = true; //Set nesting depth trackOpBit = 1 << ++effectTrackDepth; if (effectTrackDepth <= maxMarkerBits) { initDepMarkers(this); } else { cleanupEffect(this); } //Execute the side effects of effect return (); } finally { //Exit the execution of the current effect callback function and return the global variable to the current //The parent effect of effect (traceback) if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this); } //Retrieve all trackOpBit = 1 << --effectTrackDepth; //Restore trackOpBit activeEffect = ; shouldTrack = lastShouldTrack; = undefined; if () { (); } } } stop() { if (activeEffect === this) { = true; } else if () { cleanupEffect(this); if () { (); } = false; } } }
- The nested effect activeEffect pointing problem was solved by try finally.
- Setting effectOpBit indicates the current depth, and if it exceeds 30 layers, it cannot be nested anymore.
- The stop method is used to stop the execution of side effects.
- Next, we continue to watch trackEffects execution.
//Collect side effectsexport function trackEffects(dep, debuggerEventExtraInfo) { let shouldTrack = false; if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { |= trackOpBit; shouldTrack = !wasTracked(dep); } } else { //If it has been collected, it will not be collected shouldTrack = !(activeEffect); } //Judge whether it is necessary to collect it through the above if (shouldTrack) { //Add effect to the dep corresponding to the current target key (activeEffect); //The dep of what variables are proxyed in the current effect (dep); //Life cycle, when the track is actually executed, the function is called if () { ({ effect: activeEffect, ...debuggerEventExtraInfo, }); } } }
- Obviously, this function is used to collect effect to dep and build effect deps at the same time (represents the dep that is pointed to by the proxy variable in the current effect, for example, it can point to a dep, and is executed in the current effect callback function. So for the current effect, deps should contain the dep represented)
- After completing the dependency collection, we can enter the setter study! Trigger dependency update.
(2).setter in handlers
//Create the factory function of setterexport function createSetter(shallow) { return function set(target, key, value, receiver) { let oldValue = target[key]; //Get the value before the proxy object //The old value is ref, the new value is not ref if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) { return false; } //The situation of deep proxy if (!shallow) { if (!isShallow(value) && !isReadonly(value)) { //Prevent the secondary setter if the value is operated later oldValue = toRaw(oldValue); value = toRaw(value); } //target is an object and the value is ref type. It should be modified when modifying this value. if (!isArray(target) && isRef(oldValue) && !isRef(value)) { = value; return true; } } //Judge whether the currently accessed key exists. If it does not exist, it is to set a new value const hadKey = //The current target is an array and the number is accessed isArray(target) && isIntegerKey(key) ? Number(key) < : hasOwn(target, key); //Set value const result = (target, key, value, receiver); if (target === toRaw(receiver)) { //Set new value if (!hadKey) { trigger(target, , key, value); } //Modify the old value else if (hasChanged(value, oldValue)) { trigger(target, , key, value, oldValue); } } return result; }; }
- According to hadKey, determine whether the value is currently modified or added, and pass different types to trigger (trigger update) so we will continue to analyze trigger next
//Add necessary side effects to deps according to different typesexport function trigger(target, type, key, newValue, oldValue, oldTarget) { const depsMap = (target); if (!depsMap) { return; } let deps = []; //All dependencies to be processed if (type === ) { //Clear, it means that all elements have changed // Therefore, all need to be added to dependencies deps = [...()]; } //Intercept the situation of modifying the array length else if (key === "length" && isArray(target)) { //Put the key as length or the array subscript is greater than the set value so depend on //For example: const a = [1,2,3] =1 //Then the array length has changed, and the dependencies of 2 and 3 should be put into ((dep, key) => { if (key === "length" || key >= newValue) { (dep); } }); } //Other situations to obtain the dependencies collected in getter into deps else { //Put the dependency pointed to by target key into deps if (key !== void 0) { ((key)); } //Add different necessary dependencies to deps according to different types switch (type) { // Processing to add new values case : if (!isArray(target)) { //set or map ((ITERATE_KEY)); if (isMap(target)) { ((MAP_KEY_ITERATE_KEY)); } } else if (isIntegerKey(key)) { //The current modified array and new value is added //For example = 3 arr[4] = 8 //At this time the array length will change so the current array //The length attribute still needs to be put into dependencies (("length")); } break; case : //Processing delete... break; case : //Processing map type... } } //The current effect information const eventInfo = { target, type, key, newValue, oldValue, oldTarget }; if ( === 1) { if (deps[0]) { { triggerEffects(deps[0], eventInfo); } } } else { const effects = []; //Flat all effects for (const dep of deps) { if (dep) { (...dep); } } //Execute all side effects triggerEffects(createDep(effects), eventInfo); } } //Create depexport const createDep = (effects) => { const dep = new Set(effects); = 0; = 0; return dep; };
- This function obviously handles the marginal situation, collects all deps and calls triggerEffects to trigger.
- triggerOpTypes has a total of "clear", "set", "delete", and "add", among which only "add" is the proxy that handles object and array.
- "clear": When clear is triggered, it means clearing all elements of the current proxy object, all elements have been modified, so all dep needs to be added to deps.
- "add": means that the value is currently added. For the array, if a property with a longer length is accessed, the length attribute will be modified. Therefore, in this case, the dep corresponding to the attribute "length" should also be put into deps.
- Assume that the array length is 10, and then the array length attribute is modified, for example = 3, it is equivalent to deleting 7 elements, and the corresponding dep of these 7 elements should be put into deps.
- Next, continue to call triggerEffects to trigger all the collected dep.
// Deps final composed of trigger trigger all side effects executionfunction triggerEffects(dep, debuggerEventExtraInfo) { //Get all the effects and wrap them into an array const effects = isArray(dep) ? dep : [...dep]; //Execute first with computed attribute for (const effect of effects) { if () { triggerEffect(effect, debuggerEventExtraInfo); } } //Execute after not containing the computed attribute for (const effect of effects) { if (!) { triggerEffect(effect, debuggerEventExtraInfo); } } } function triggerEffect(effect, debuggerEventExtraInfo) { if (effect !== activeEffect || ) { //Life cycle, call this effect when triggering if () { (({ effect }, debuggerEventExtraInfo)); } //If there is a scheduler, execute the scheduler, otherwise execute run if () { (); } else { (); } } }
- If there is a computed property, execute first, and if there is no, execute later. If there is a scheduler calling scheduler, otherwise run will be called. This completes the trigger.
(3).handlers' deleteProperty
//The logic of processing the deletion attributes (unified processing)//target: The object to delete the attribute key: The key to delete the object valueexport function deleteProperty(target, key) { const hadKey = hasOwn(target, key); //Judge whether the deleted attribute is const oldValue = target[key]; //Get old value //Remove the return value of the attribute is whether it is deleted successfully const result = (target, key); if (result && hadKey) { //Trigger side effects trigger(target, , key, undefined, oldValue); } return result; }
When delete is called, deleteProperty will listen. This is obviously a case of modifying the value. So when we execute trigger, the type is naturally "delete". I still remember that we did not explain the "delete" type in trigger. Let's see how to deal with this part.
//Put the dependency pointed to by target key into deps if (key !== void 0) { ((key)); } //Omit some code...case : if (!isArray(target)) { //Add the dependency of key as iterate, and then talk about where this dependency comes from ((ITERATE_KEY)); if (isMap(target)) { ((MAP_KEY_ITERATE_KEY)); } } break; //Omit some code...
- First put the dependency of the deleted element into the deps.
- If the object is deleted, the dependency with key as ITERATE_KEY will be added. This key comes from the interception of ownKeys. When collecting dependencies, it is written, or called in effect. Such code will trigger the interception of ownKeys at this time, the track type is actually ITERATE_KEY. That is to say, if you write it, you will collect dependencies. One day you delete the attributes on proxy, which will also trigger the dependency update.
const {reactive,effect} = require('./') const proxy = reactive({a:1}) effect(()=>{ (proxy) (111) }) effect(()=>{ (111) }) delete //log: 111 111 111 111
(4).handlers' ownKeys
//Intercept getOwnPropertyNames, etc.export function ownKeys(target) { track(target, "iterate", isArray(target) ? "length" : ITERATE_KEY); return (target); }
- track We have analyzed that if the target is a non-array element, then the tracking key is ITERATE_KEY, which is where delete comes from above.
(5).Has of handlers
//Intercept foo in proxy foo in (proxy)//with(proxy){foo} export function has(target, key) { const result = (target, key); //Judge whether there is this attribute //Not Symbol or built-in Symbol attribute if (!isSymbol(key) || !(key)) { track(target, "has", key); } return result; }
- has also judges whether there is an element, and does not involve modification, so it is a track, the type of pass is "has", and the dependencies can be collected. The special thing is that has can only intercept the situation in the annotation, and getOwnProperty cannot intercept it.
OK! The second part has completed all the analysis, but this article is not finished yet! Because the length of the article is too long, I will put the third and fourth parts in the next chapter. Let’s summarize it in the end!
Summary of this article:
In this article, we wrote a simple version of reactivity to facilitate everyone to understand the real source code in the future. Then we analyzed how to intercept data of array and object types. Generally speaking, it is to modify the current activeEffect's pointer when the effect is executed, and then when the effect is executed, the operation of the get has ownKeys is collected through the proxy native API, and the dependency is collected. Then it is triggered when the set and delete is set, and the marginal situation is also processed, such as modifying the length of the array access, and processing using the pop push include method.
Below we will continue to analyze the interception of map set weakMap weakSet and the implementation of APIs such as ref computered. For more information about vue3 source code analysis reactivity, please pay attention to my other related articles!