In the last chapter, we know how components are mounted and rendered, but one question remains: How is the responsive data collected as an effect object? In this chapter, we start with two APIs for declaring responsive data: reactive and ref:
reactive
// packages/reactivity/src/reactive.ts export function reactive(target: object) {<!-- --> // if trying to observe a readonly proxy, return the readonly version. if (isReadonly(target)) {<!-- --> return target } return createReactiveObject( target, false, mutableHandlers, // handlers for common objects mutableCollectionHandlers, // handlers for collection objects reactiveMap ) }
The actual call is createReactiveObject
createReactiveObject
// Create a responsive object function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any> ) {<!-- --> if (!isObject(target)) {<!-- --> // Determine whether it is an object type if (__DEV__) {<!-- --> 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 it has been proxied, return directly if ( target[ReactiveFlags.RAW] & amp; & amp; !(isReadonly & amp; & amp; target[ReactiveFlags.IS_REACTIVE]) ) {<!-- --> return target } // target already has corresponding Proxy const existingProxy = proxyMap. get(target) // If the proxy object already exists, return directly if (existingProxy) {<!-- --> return existingProxy } // only specific value types can be observed. const targetType = getTargetType(target) // Only specific value types can be observed, and return directly if they do not match if (targetType === TargetType.INVALID) {<!-- --> return target } // create proxy object const proxy = new Proxy( target, // If the type is a collection, use the collection processor, otherwise use the basic processor (collection type: Map Set WeakMap WeakSet) targetType === TargetType. COLLECTION ? collectionHandlers : baseHandlers ) proxyMap.set(target, proxy) return proxy }
Proxy is used here to proxy the target, and the handler is judged according to the incoming type. We generally use baseHandlers, which are actually incoming mutableHandlers. PS: If you are not familiar with Proxy API, please get familiar with it first
export const mutableHandlers: ProxyHandler<object> = {<!-- --> get, set, deleteProperty, has, // handle in operator ownKeys // handle Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols(), for...in }
We mainly look at get and set handlers, which are the most commonly used proxy executions triggered when we get and set values. They are created by createGetter and createSetter respectively.
createGetter
// packages/reactivity/src/baseHandlers.ts // create get handler function createGetter(isReadonly = false, shallow = false) {<!-- --> return function get(target: Target, key: string | symbol, receiver: object) {<!-- --> // some boundary handling ... // Whether target is an array const targetIsArray = isArray(target) if (!isReadonly) {<!-- --> if (targetIsArray & amp; & amp; hasOwn(arrayInstrumentations, key)) {<!-- --> // Special handling of arrays arrayInstrumentations is the rewritten array method object return Reflect.get(arrayInstrumentations, key, receiver) } if (key === 'hasOwnProperty') {<!-- --> return hasOwnProperty } } // return value processing const res = Reflect. get(target, key, receiver) if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {<!-- --> return res } if (!isReadonly) {<!-- --> // capture dependencies track(target, TrackOpTypes. GET, key) } if (shallow) {<!-- --> return res } if (isRef(res)) {<!-- --> // ref unwrapping - skip unwrap for Array + integer key. return targetIsArray & amp; & amp; isIntegerKey(key) ? res : res.value } if (isObject(res)) {<!-- --> // When the return value is an object, execute reactive (recursive operation) return isReadonly? readonly(res) : reactive(res) } return res } }
The core here is the execution of the track function, which will perform the collection effect operation. Let’s look at the specific implementation:
track
// packages/reactivity/src/effect.ts export function track(target: object, type: TrackOpTypes, key: unknown) {<!-- --> if (shouldTrack & amp; & amp; activeEffect) {<!-- --> // targetMap is a WeakMap instance let depsMap = targetMap. get(target) if (!depsMap) {<!-- --> // If there is no depsMap, set target as the key value of targetMap and the value is a Map targetMap.set(target, (depsMap = new Map())) } // get dep let dep = depsMap. get(key) if (!dep) {<!-- --> // If there is no dep, set the key to be the key of depsMap. The value is a Set collection. Here is the effect collection collected by the key. depsMap.set(key, (dep = createDep())) } const eventInfo = __DEV__ ? {<!-- --> effect: activeEffect, target, type, key } : undefined // Collect effects into dep trackEffects(dep, eventInfo) } }
trackEffects
export function trackEffects( dep: Dep, debuggerEventExtraInfo?: 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!) } // only track it if it's not already being tracked if (shouldTrack) {<!-- --> // Collect effect side effects dep. add(activeEffect!) // effect collects dep for cleanup activeEffect!.deps.push(dep) if (__DEV__ & amp; & amp; activeEffect!.onTrack) {<!-- --> activeEffect!.onTrack( extend( {<!-- --> effect: activeEffect! }, debuggerEventExtraInfo! ) ) } } }
The logic here is also very simple. The only doubt may be where the activeEffect comes from. In fact, activeEffect is the instance we declared using the ReactiveEffect class. When we execute effect.run, it will assign activeEffect to effect. Let’s take a look at the specific implementation
class ReactiveEffect {<!-- --> ... run() {<!-- --> if (!this.active) {<!-- --> return this.fn() } let parent: ReactiveEffect | undefined = activeEffect let lastShouldTrack = shouldTrack while (parent) {<!-- --> if (parent === this) {<!-- --> return } parent = parent.parent } try {<!-- --> // record activeEffect this.parent = activeEffect // Assign activeEffect to this where this points to the instance, which is the effect we instantiated activeEffect = this // Set shouldTrack to true before executing fn shouldTrack = true trackOpBit = 1 << + + effectTrackDepth if (effectTrackDepth <= maxMarkerBits) {<!-- --> initDepMarkers(this) } else {<!-- --> cleanupEffect(this) } return this.fn() } finally {<!-- --> if (effectTrackDepth <= maxMarkerBits) {<!-- --> finalizeDepMarkers(this) } trackOpBit = 1 << --effectTrackDepth // activeEffect is reassigned to this.parent here to deal with the nesting of effects with the operation in the try code block activeEffect = this. parent shouldTrack = lastShouldTrack this.parent = undefined if (this. deferStop) {<!-- --> this. stop() } } } ... }
So far we already know how the responsive data is collected as an effect object. When we execute the function update function, execute render, trigger the get of responsive data, and then the dep corresponding to the corresponding key will collect the current effect .
The relationship between responsive objects and dep in data structure: Vue3 uses a targetMap global WeakMap instance to store, its key is our target, and its value is a Map instance. The key of the changed Map instance is the key of the target, and the value is corresponding The effect collection (Set) collected by the key.
The above is the collection process, let’s look at the trigger process:
createSetter
function createSetter(shallow = false) {<!-- --> return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean {<!-- --> // Get the old value first let oldValue = (target as any)[key] if (isReadonly(oldValue) & amp; & amp; isRef(oldValue) & amp; & amp; !isRef(value)) {<!-- --> return false } if (!shallow) {<!-- --> if (!isShallow(value) & amp; & amp; !isReadonly(value)) {<!-- --> oldValue = toRaw(oldValue) value = toRaw(value) } if (!isArray(target) & amp; & amp; isRef(oldValue) & amp; & amp; !isRef(value)) {<!-- --> oldValue. value = value return true } } else {<!-- --> // in shallow mode, objects are set as-is regardless of reactive or not } const hadKey = isArray(target) & amp; & amp; 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 // don't fire if target is something in the prototype chain if (target === toRaw(receiver)) {<!-- --> if (!hadKey) {<!-- --> // trigger add without key trigger(target, TriggerOpTypes. ADD, key, value) } else if (hasChanged(value, oldValue)) {<!-- --> // Trigger set when there is a key trigger(target, TriggerOpTypes. SET, key, value, oldValue) } } return result } }
Eventually the trigger will be triggered:
trigger
// trigger effect function export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) {<!-- --> // get depsMap const depsMap = targetMap. get(target) if (!depsMap) {<!-- --> // never been tracked return } // Create an array of deps to execute let deps: (Dep | undefined)[] = [] if (type === TriggerOpTypes. CLEAR) {<!-- --> // collection being cleared // trigger all effects for target // Trigger the effect function corresponding to all keys when the array or map is cleared deps = [...depsMap. values()] } else if (key === 'length' & amp; & amp; isArray(target)) {<!-- --> const newLength = Number(newValue) depsMap.forEach((dep, key) => {<!-- --> if (key === 'length' || key >= newLength) {<!-- --> deps.push(dep) } }) } else {<!-- --> // schedule runs for SET | ADD | DELETE if (key !== void 0) {<!-- --> // set add delete operation Add the effect function corresponding to the key to the deps array deps.push(depsMap.get(key)) } // also run for iteration key on ADD | DELETE | // Push the corresponding dep according to different operations switch (type) {<!-- --> case TriggerOpTypes. ADD: if (!isArray(target)) {<!-- --> // Add the effect function of the loop operation to the deps array 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 // Execute triggerEffects Execute all effects in dep 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)) } } }
triggerEffects
export function triggerEffects( dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo ) {<!-- --> // spread into array for stabilization const effects = isArray(dep) ? dep : [...dep] // The computed effect will be executed first // Prevent render from obtaining computed values when _dirty has not been set to true for (const effect of effects) {<!-- --> if (effect.computed) {<!-- --> triggerEffect(effect, debuggerEventExtraInfo) } } for (const effect of effects) {<!-- --> if (!effect.computed) {<!-- --> triggerEffect(effect, debuggerEventExtraInfo) } } } // execute effect function triggerEffect( effect: ReactiveEffect, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) {<!-- --> if (effect !== activeEffect || effect.allowRecurse) {<!-- --> if (__DEV__ & amp; & amp; effect.onTrigger) {<!-- --> effect.onTrigger(extend({<!-- --> effect }, debuggerEventExtraInfo)) } // If the effect has a scheduler, execute the scheduler, otherwise execute the run and eventually execute the function update function to update if (effect.scheduler) {<!-- --> effect. scheduler() } else {<!-- --> effect. run() } } }
The component’s scheduler uses queueJob, which uses an asynchronous method to optimize the performance of multiple changes in responsive data triggering multiple function update function executions in a synchronous update, so that the function update function is only executed once. There is no specific development here. Those who are interested can see the specific source code implementation, which is not complicated.
ref
ref is aimed at the responsive processing of a single value, which is simpler and does not have the problem of loss of reactive responsiveness. Let’s see its specific implementation:
// packages/reactivity/src/ref.ts export function ref(value?: unknown) {<!-- --> // create ref return createRef(value, false) } function createRef(rawValue: unknown, shallow: boolean) {<!-- --> if (isRef(rawValue)) {<!-- --> return rawValue } // Instantiate the RefImpl class and return return new RefImpl(rawValue, shallow) } class RefImpl<T> {<!-- --> private_value: T private_rawValue: T // dependency collection public dep?: Dep = undefined public readonly __v_isRef = true // Constructor constructor(value: T, public readonly __v_isShallow: boolean) {<!-- --> this._rawValue = __v_isShallow? value : toRaw(value) // value can also be a complex data type, it will execute reactive API to make it responsive this._value = __v_isShallow ? value : toReactive(value) } get value() {<!-- --> // collect dependencies trackRefValue(this) return this._value } set value(newVal) {<!-- --> const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal) newVal = useDirectValue ? newVal : toRaw(newVal) if (hasChanged(newVal, this._rawValue)) {<!-- --> this._rawValue = newVal this._value = useDirectValue ? newVal : toReactive(newVal) // Trigger dependencies triggerRefValue(this, newVal) } } }
You can see that ref returns a RefImpl instance, which uses the get set accessor to collect dependencies in get and trigger dependencies in set. The difference from reactive is that the dependency collection of ref is stored in its own dep attribute, not globally The targetMap object. Next, look at the specific trackRefValue and triggerRefValue operations:
trackRefValue
// packages/reactivity/src/ref.ts export function trackRefValue(ref: RefBase<any>) {<!-- --> if (shouldTrack & amp; & amp; activeEffect) {<!-- --> ref = toRaw(ref) if (__DEV__) {<!-- --> // call trackEffects trackEffects(ref.dep || (ref.dep = createDep()), {<!-- --> target: ref, type: TrackOpTypes. GET, key: 'value' }) } else {<!-- --> trackEffects(ref.dep || (ref.dep = createDep())) } } }
Finally, trackEffects is called. We also called this function during the reactive collection process. They are public.
triggerRefValue
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {<!-- --> ref = toRaw(ref) const dep = ref.dep if (dep) {<!-- --> if (__DEV__) {<!-- --> // trigger the effect triggerEffects(dep, {<!-- --> target: ref, type: TriggerOpTypes.SET, key: 'value', newValue: newVal }) } else {<!-- --> triggerEffects(dep) } } }
Here is also the dependency of reusing triggerEffects to execute collection