target
vue3 responsive source code analysis
Vue3's data response and hijacking are implemented based on Proxy objects supported by modern browsers.
The following example briefly illustrates the principle of vue3's responsive data, that is, get and set are hijacked through Proxy objects, and the hijacking method is inserted between the value and assignment, that is, track and trigger - dependent tracking and the triggering of side effects.
const initData = {value: 1} const proxy = new Proxy( initData, // Proxied object { // handler object get(target, key) { // track return target[key] } set(target, key, value) { // trigger return Reflect.set(target, key, value); } } ) // proxy directly accesses the modified object // It can also be called reactive/ref
Basic description
track: Collect dependencies trigger: Trigger side effect function
code analysis
Run the following vue3 example code to analyze the source code step by step.
// setup import {ref, reactive, effect, computed} from "vue" export default { ... setup(props, context) { const countRef = 0; const number = reactive({ num: 1 }) effect(() => { console.log(countRef.value) }) const increment = () => { countRef.value++ } const result = computed(() => number.num ** 2) return { countRef, number, result, increment } } }
First, use the flowchart to briefly describe the execution process of the above code:
For component initialization, first declare the countRef variable and execute the effect function. Effect will call the passed in parameter function fn, which is executed to countRef Value will be hijacked by getter s and track ed. In this process, there will be a global variable targetMap to associate countRef with FN. When the value of countRef changes, the following process is triggered:
When the value of countRef changes, it will be hijacked by setter to trigger. Simply speaking, it is the process of obtaining the fn corresponding to countRef from the global variable targetMap, and then executing the fn function. Of course, many details are omitted here.
Initialization phase
- Create responsive data
const countRef = ref(0) const number = reactive({num: 1})
function ref(value) { return createRef(value, false); } function createRef(rawValue, shallow) { // Judge whether it is a Ref type object. If yes, it will be returned directly if (isRef(rawValue)) { return rawValue; } return new RefImpl(rawValue, shallow); }
The createRef function determines whether the incoming parameter rawValue is a reference type. If not, it returns an instance of the RefImpl class wrapper.
- RefImpl class
class RefImpl { constructor(value, _shallow) { this._shallow = _shallow; this.dep = undefined; this.__v_isRef = true; this._rawValue = _shallow ? value : toRaw(value); this._value = _shallow ? value : convert(value); } get value() { trackRefValue(this); // track to collect dependencies return this._value; } set value(newVal) { newVal = this._shallow ? newVal : toRaw(newVal); if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal; this._value = this._shallow ? newVal : convert(newVal); triggerRefValue(this, newVal); // Trigger, trigger the update when the value changes } } }
The RefImpl class implements track and trigger through get/set. This also explains the wrapped value of ref -- accessed through the value attribute. Look at the reactive method based on the same idea
function reactive(target) { // if trying to observe a readonly proxy, return the readonly version. // If the target object's__ v_isReadonly property is true, and the original object is returned directly if (target && target["__v_isReadonly" /* IS_READONLY */ ]) { return target; } return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap); } const mutableHandlers = { get, set, deleteProperty, has, ownKeys }; const mutableCollectionHandlers = { get: /*#__PURE__*/ createInstrumentationGetter(false, false) }; // Global map const reactiveMap = new WeakMap(); // Main logic function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) { if (!isObject(target)) { { console.warn(`value cannot be made reactive: ${String(target)}`); } return target; } // target is already a Proxy, return it. // exception: calling readonly() on a reactive object if (target["__v_raw" /* RAW */ ] && !(isReadonly && target["__v_isReactive" /* IS_REACTIVE */ ])) { return target; } // Focus on the following code logic: // target already has corresponding Proxy const existingProxy = proxyMap.get(target); if (existingProxy) { return existingProxy; } // only a whitelist of value types can be observed. const targetType = getTargetType(target); if (targetType === 0 /* INVALID */ ) { return target; } const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers); proxyMap.set(target, proxy); return proxy; }
The createReactive method is the main logic, and the "shallowReactive" method for creating shallow response and the "readonly" method for read-only are all connected to this function.
As you can see, target: {num: 1} is represented here. If it has been proxied before (there is cache in proxyMap), it will be returned directly. Otherwise, it will be cached and returned. The reactive method enables Proxy to implement Proxy.
Data tracking
effect is executed and the callback method fn is called because fn internally accesses the value attribute of countRef
effect(() =>{ console.log(countRef.value) })
The get method defined by the class RefImpl is triggered here
class RefImpl { get value() { trackRefValue(this); return this._value; } } // This conditionally enables trackEffects to maintain the ref instance attribute dep and // The mapping of active effects, in other words, is that the packaged data is included in the effect for the first time // When the fn function is accessed, the wrapper object also saves the fn function trackRefValue(ref) { if (isTracking()) { ref = toRaw(ref); // If this is the first access and the dep attribute is undefined, create a dep Attribute Collection dependency if (!ref.dep) { ref.dep = createDep(); } { // Key point: trigger side effect function trackEffects(ref.dep, { target: ref, type: "get" /* GET */ , key: 'value' }); } } } function trackEffects(dep, debuggerEventExtraInfo) { let shouldTrack = false; if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { dep.n |= trackOpBit; // set newly tracked shouldTrack = !wasTracked(dep); } } else { // Full cleanup mode. shouldTrack = !dep.has(activeEffect); } // activeEffect is a global variable. When executing an effect, it will point to an instance containing fn. // In other words, here dep.add(activeEffect) // Equivalent to ref.dep.add(wrapper(fn)), wrapper is the simplification of the process if (shouldTrack) { dep.add(activeEffect); // Make a mark coordinate1 activeEffect.deps.push(dep); if (activeEffect.onTrack) { activeEffect.onTrack( Object.assign( { effect: activeEffect, }, debuggerEventExtraInfo ) ); } } }
Status update phase
Take the data source created by ref as an example, countref Value++ start from the bottom face
class RefImpl { set value(newVal) { newVal = this._shallow ? newVal : toRaw(newVal); if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal; this._value = this._shallow ? newVal : convert(newVal); triggerRefValue(this, newVal); // trigger triggered by value update } } } function triggerRefValue(ref, newVal) { ref = toRaw(ref); // If the ref has no dep attribute, it indicates that dependency collection has not been triggered before. if (ref.dep) { // Go back to the place marked face coordinate1 { triggerEffects(ref.dep, { target: ref, type: "set" /* SET */, key: "value", newValue: newVal, }); } } }
The position of the tag proves that the packing value ref(0) has a reference relation to the fn to be executed in the future through dep, and the triggerEffect method will trigger it once set according to this relation!
function triggerEffects(dep, debuggerEventExtraInfo) { // spread into array for stabilization for (const effect of isArray(dep) ? dep : [...dep]) { if (effect !== activeEffect || effect.allowRecurse) { if (effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)); } if (effect.scheduler) { effect.scheduler(); // This is where fn executes in the micro task queue } else { effect.run(); // This is the location of fn synchronous execution } } } }
In the data tracking phase, the dep attribute of ref is associated with the executed fn through activeEffect, and then in the state update phase, the fn associated with def executing ref is traversed again After clearing the main line and paying a little attention to the logic of effect, you can connect scheduler, run and fn:
function effect(fn, options) { if (fn.effect) { fn = fn.effect.fn; } const _effect = new ReactiveEffect(fn); if (options) { extend(_effect, options); if (options.scope) recordEffectScope(_effect, options.scope); } // effect(fn) first execution if (!options || !options.lazy) { _effect.run(); } const runner = _effect.run.bind(_effect); runner.effect = _effect; return runner; }
The fn function parameter passed in here is wrapped as an instance by the ReactiveEffect class
const effectStack = []; class ReactiveEffect { constructor(fn, scheduler = null, scope) { this.fn = fn; this.scheduler = scheduler; this.active = true; this.deps = []; recordEffectScope(this, scope); } run() { if (!this.active) { return this.fn(); } if (!effectStack.includes(this)) { // Execute when the instance is not in the stack to avoid repeated triggering of executionfn try { effectStack.push((activeEffect = this)); enableTracking(); trackOpBit = 1 << ++effectTrackDepth; if (effectTrackDepth <= maxMarkerBits) { initDepMarkers(this); } else { cleanupEffect(this); } return this.fn(); } finally { if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this); } trackOpBit = 1 << --effectTrackDepth; resetTracking(); effectStack.pop(); const n = effectStack.length; activeEffect = n > 0 ? effectStack[n - 1] : undefined; } } } stop() { if (this.active) { cleanupEffect(this); if (this.onStop) { this.onStop(); } this.active = false; } } }
The entire process of creating data and updating data in the ref method in the previous section. In fact, the data created by reactive also has a similar logic. The difference lies in the handler part of the Proxy:
const proxy = new Proxy( target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers );
Take baseHandlers as an example (this_is a formal parameter). Find the actual parameter mutableHandlers,
const mutableHandlers = { get, set, deleteProperty, has, ownKeys, }; // We can conclude the get/set of_is the place to enter line track and trigger. Find it const get = /*#__PURE__*/ createGetter(); function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { if (key === "__v_isReactive" /* IS_REACTIVE */) { return !isReadonly; } else if (key === "__v_isReadonly" /* IS_READONLY */) { return isReadonly; } else if ( key === "__v_raw" /* RAW */ && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target; } const targetIsArray = isArray(target); if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { // There are also track s in arrayInstrumentations. They are no longer displayed, but focus on the main line 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, "get" /* GET */, key); // The same logic as ref interception appears } ... return res; }; } function track(target, type, key) { if (!isTracking()) { 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())); } const eventInfo = { effect: activeEffect, target, type, key }; trackEffects(dep, eventInfo); // Same goal as trackRefValue, omitted }
- set
const set = /*#__PURE__*/ createSetter() function createSetter(shallow = false) { return function set(target, key, value, receiver) { let oldValue = target[key]; if (!shallow) { value = toRaw(value); oldValue = toRaw(oldValue); if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value; return true; } } 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)) { if (!hadKey) { trigger(target, "add" /* ADD */, key, value); // trigger like ref } else if (hasChanged(value, oldValue)) { trigger(target, "set" /* SET */, key, value, oldValue); } } return result; }; } function trigger(target, type, key, newValue, oldValue, oldTarget) { const depsMap = targetMap.get(target); if (!depsMap) { // never been tracked return; } let deps = []; if (type === "clear" /* 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) { 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 "add" /* 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 "delete" /* DELETE */: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)); if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)); } } break; case "set" /* SET */: if (isMap(target)) { deps.push(depsMap.get(ITERATE_KEY)); } break; } } function trigger(target, type, key, newValue, oldValue, oldTarget) { ... const eventInfo = { target, type, key, newValue, oldValue, oldTarget }; if (deps.length === 1) { if (deps[0]) { { triggerEffects(deps[0], eventInfo); // Same goal as triggerRefValue, omitted } } } else { const effects = []; for (const dep of deps) { if (dep) { effects.push(...dep); } } { triggerEffects(createDep(effects), eventInfo); } } }
In fact, the watch method is also a package based on effect, so I won't repeat it.