Principles of reading source code
- Simplify and debug, master the core process
- Don’t be bound by too many details, just focus on the main line
Source code implementation of reactive and effect methods
1) User code example, the business code is as follows:
<script src='../../dist/vue.global.js'></script> <body> <div id='app'></div> </body> <script> const {<!-- --> reactive, effect } = Vue const obj = reactive({<!-- --> name: 'Zhang San' }) effect(()=>{<!-- --> document.querySelector('#app').innerText = obj.name // Note, here is the getter behavior }) const timer = setTimeout(() => {<!-- --> clearTimeout(timer) obj.name = 'Li Si' // here is the setter behavior }, 2000) </script>
In the above code, we pass in an object in reactive to construct an obj object, and pass in a callback function in effect
We want to trace the source from the user sample code and see how reactive and effect are executed internally
2) Perform reactive debug
-
Now find reactive.ts in the Pages sub-panel of Chrome’s Sources panel, find the reactive() method, and set a breakpoint
-
Refresh the page and go to the breakpoint, the program goes like this:
export function reactive(target: object) {<!-- --> // if trying to observe a readonly proxy, return the readonly version. // skip here if (isReadonly(target)) {<!-- --> return target } // go straight here return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap ) }
-
The code enters the createReactiveObject method
function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any> ) {<!-- --> // skip here if (!isObject(target)) {<!-- --> 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 there is no match here, it will be skipped if ( target[ReactiveFlags.RAW] & amp; & amp; !(isReadonly & amp; & amp; target[ReactiveFlags.IS_REACTIVE]) ) {<!-- --> return target } // target already has corresponding Proxy // Here an instance is read from proxyMap, where proxyMap corresponds to a WeakMap, we treat it as an ordinary map object for the time being const existingProxy = proxyMap. get(target) // Can't read here, will skip if (existingProxy) {<!-- --> return existingProxy } // only specific value types can be observed. // try to read targetType here, the current value is 1 const targetType = getTargetType(target) // If there is no match here, you can skip it directly if (targetType === TargetType.INVALID) {<!-- --> return target } // Here is the point: create a proxy object const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers // Mainly look at the ternary operation here to determine whether the current targetType is the specified value ) // Here, store the target in the map to establish a corresponding relationship proxyMap.set(target, proxy) return proxy // finally return this proxy object }
-
The targetType mentioned above is an enumeration object
const enum TargetType {<!-- --> INVALID = 0, COMMON = 1, COLLECTION = 2 }
-
Let’s jump to the baseHandlers method to see what’s going on, look up the code, it’s the third parameter of createReactiveObject
- That is, the third parameter of return createReactiveObject in the reactive method: mutableHandlers
- And mutableHandlers is defined in baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {<!-- --> get, set, deleteProperty, has, ownKeys }
- There are two key attributes in the above code, get and set, here we need to consider the timing when the getter and setter behaviors in Proxy are triggered
- We need to pay attention to the getter and setter in our own business code, once they are triggered, the following two functions will be executed
- First look at get
const get = /*#__PURE__*/ createGetter()
- This get is a return of createGetter, which returns a get function, which is the get
function createGetter(isReadonly = false, shallow = false) {<!-- --> in the above Handlers return function get(target: Target, key: string | symbol, receiver: object) {<!-- --> // The judgment of the following columns will be skipped 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 & amp; & amp; receiver === (isReadonly ? shallow ?shallowReadonlyMap : readonlyMap : shallow ?shallowReactiveMap : reactiveMap ).get(target) ) {<!-- --> return target } // mismatches here will also be skipped const targetIsArray = isArray(target) // mismatches here will also be skipped if (!isReadonly & amp; & amp; targetIsArray & amp; & amp; hasOwn(arrayInstrumentations, key)) {<!-- --> return Reflect.get(arrayInstrumentations, key, receiver) } // Here Reflect.get returns the key of the receiver const res = Reflect. get(target, key, receiver) // mismatches here will also be skipped if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {<!-- --> return res } // It will be executed here, pay attention to the track function here, mainly for dependency collection and dependency triggering // track here is a process of dependency collection if (!isReadonly) {<!-- --> 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)) {<!-- --> // 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) } // Finally, it will be executed here, and the res result will be returned return res } }
- First look at get
-
Let’s see what happens to the track in the above getter, in effect.ts
export function track(target: object, type: TrackOpTypes, key: unknown) {<!-- --> // Note here that activeEffect is assigned in the run function of the ReactiveEffect class; shouldTrack is also assigned true, so it will be executed here if (shouldTrack & amp; & amp; activeEffect) {<!-- --> // The targetMap here is also a WeekMap, and the get here is undefined let depsMap = targetMap. get(target) // Because it is undefined, it will be executed here to perform an assignment operation on targetMap if (!depsMap) {<!-- --> targetMap.set(target, (depsMap = new Map())) // At this time, the targetMap here has a value, with the target object as the key and a new Map object as the value } // Here, as above, the value cannot be retrieved, here is also undefined let dep = depsMap. get(key) // here will execute if (!dep) {<!-- --> // At this time, use the key as the key, and store createDep() as the value in the map depsMap. The dep here is essentially a set. Refer to the source code implementation of createDep depsMap.set(key, (dep = createDep())) } const eventInfo = __DEV__ ? {<!-- --> effect: activeEffect, target, type, key } : undefined // Note here, trackEffects(dep, eventInfo) } }
-
Through the above code, we can know that the WeekMap targetMap is quite complicated, and its structure is as follows:
- targetMap
- key: target
- value: Map
- key: key
- value: Set
- targetMap
-
Let’s take a look at what happened to the above createDep(), in dep.ts
export const createDep = (effects?: ReactiveEffect[]): Dep => {<!-- --> // here is a set collection const dep = new Set<ReactiveEffect>(effects) as Dep dep.w = 0 dep.n = 0 return dep }
-
Let’s look at what happened to 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!) } // follow here if (shouldTrack) {<!-- --> // The dep here is essentially a Set, and the effect instance is added to this Set to save // The following two lines establish the connection between dep and activeEffect, so that we can get the current activeEffect through the key of the proxy object, that is, the ReactiveEffect instance. Here we have completed the dependency collection dep. add(activeEffect!) activeEffect!.deps.push(dep) // From the above two steps, the ReactiveEffect instance is saved in our targetMap object if (__DEV__ & amp; & amp; activeEffect!.onTrack) {<!-- --> activeEffect!.onTrack({<!-- --> effect: activeEffect!, ...debuggerEventExtraInfo! }) } } }
-
As can be seen from the above, the process of relying on collection
- In essence, it is to establish the relationship between targetMap and ReactiveEffect, so that we can find the ReactiveEffect corresponding to the attribute according to the specified attribute of the specified object
- And there is a fn function in the ReactiveEffect instance, which is the first callback function of our effect, and here is the function when the getter behavior of the proxy object is currently triggered
- At this time, the specified attribute of the specified object has established a relationship with the callback function that triggers the getter behavior, and the dependency collection process is completed at this time
-
In our business code, the getter operation is performed in the effect callback, and the setter operation is performed in the setTimeout. Let’s look back at this setter
- Let’s take a look at the set again, which is similar to the above get
const set = /*#__PURE__*/ createSetter()
- Here set is a set function returned by createSetter
function createSetter(shallow = false) {<!-- --> return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean {<!-- --> 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) // Note that the result here is true, indicating that the assignment was successful 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, TriggerOpTypes. ADD, key, value) } else if (hasChanged(value, oldValue)) {<!-- --> // here will trigger trigger(target, TriggerOpTypes. SET, key, value, oldValue) } } return result } }
- Here set is a set function returned by createSetter
- Let’s take a look at the set again, which is similar to the above get
-
The core of responsiveness is the process of collecting dependencies and triggering dependencies. The above-mentioned trigger is triggering dependencies. Let’s take a look at this function, in effect.ts
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) {<!-- --> // Here, get the map based on targetMap 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' & amp; & amp; isArray(target)) {<!-- --> depsMap.forEach((dep, key) => {<!-- --> if (key === 'length' || key >= (newValue as number)) {<!-- --> deps.push(dep) } }) } else {<!-- --> // schedule runs for SET | ADD | DELETE // here will execute if (key !== void 0) {<!-- --> deps.push(depsMap.get(key)) // store the set collection in deps } // also run for iteration key on ADD | DELETE | // matching will be performed here 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 /// Matched here case TriggerOpTypes.SET: // But our target is not a map object, it will be skipped here if (isMap(target)) {<!-- --> deps.push(depsMap.get(ITERATE_KEY)) } break } } const eventInfo = __DEV__ ? {<!-- --> target, type, key, newValue, oldValue, oldTarget } : undefined // here will execute if (deps. length === 1) {<!-- --> if (deps[0]) {<!-- --> if (__DEV__) {<!-- --> // will be executed here, this is the core dependency trigger 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)) } } }
-
Let’s see what happened to triggerEffects ?
export function triggerEffects( dep: Dep | ReactiveEffect[], // This is our Set collection debuggerEventExtraInfo?: DebuggerEventExtraInfo ) {<!-- --> // spread into array for stabilization // effects here is an array of ReactiveEffects const effects = isArray(dep) ? dep : [...dep] // We can see that the following two for loops can be optimized, so it is not very meaningful to disassemble and write for (const effect of effects) {<!-- --> if (effect.computed) {<!-- --> triggerEffect(effect, debuggerEventExtraInfo) } } for (const effect of effects) {<!-- --> // here will execute if (!effect.computed) {<!-- --> triggerEffect(effect, debuggerEventExtraInfo) } } }
-
Again, we can see what happened to the triggerEffect
function triggerEffect( effect: ReactiveEffect, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) {<!-- --> if (effect !== activeEffect || effect.allowRecurse) {<!-- --> if (__DEV__ & amp; & amp; effect.onTrigger) {<!-- --> effect.onTrigger(extend({<!-- --> effect }, debuggerEventExtraInfo)) } if (effect.scheduler) {<!-- --> effect. scheduler() } else {<!-- --> // It will be executed here. In the above run code, the essence is to trigger our fn function, which is the first callback function of effect, and then trigger the getter behavior of the proxy object effect. run() } } }
-
Let’s look at the fn function in our business code
document.querySelector('#app').innerText = obj.name
- It can be seen from the above that our page will immediately update obj.name, which is the value after the setter in the timer in the business code. The above line of code will trigger the getter behavior again when obj.name is executed
- Until the new value is rendered on the page, in the whole setter behavior, the following work is mainly done
- Modify the value of obj
- Call fn saved under targetMap
- What happens when the effect callback is executed in our business code?
3) Debug the effect
-
Find effect.ts in the Pages sub-panel of the Sources panel of chrome, find the effect() method, and set a breakpoint
-
When the breakpoint is reached, the program goes like this:
// In the business code, we only pass the first parameter, the callback function export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions ): ReactiveEffectRunner {<!-- --> if ((fn as ReactiveEffectRunner).effect) {<!-- --> fn = (fn as ReactiveEffectRunner).effect.fn } // Here we pass the callback in our own business code, and construct an _effect instance based on the ReactiveEffect class const _effect = new ReactiveEffect(fn) // Here options is undefined and does not exist to skip if (options) {<!-- --> extend(_effect, options) if (options.scope) recordEffectScope(_effect, options.scope) } // Here it will match and execute the run method of the instance if (!options || !options. lazy) {<!-- --> _effect. run() } const runner = _effect.run.bind(_effect) as ReactiveEffectRunner runner. effect = _effect return runner }
-
Let’s enter the ReactiveEffect class here, take a look
export class ReactiveEffect<T = any> {<!-- --> active = true deps: Dep[] = [] parent: ReactiveEffect | undefined = undefined /** * Can be attached after creation * @internal */ computed?: ComputedRefImpl<T> /** * @internal */ allowRecurse?: boolean /** * @internal */ private deferStop?: boolean onStop?: () => void // dev only onTrack?: (event: DebuggerEvent) => void // dev only onTrigger?: (event: DebuggerEvent) => void constructor( public fn: () => T, // fn here is the function we passed in public scheduler: EffectScheduler | null = null, scope?: EffectScope ) {<!-- --> recordEffectScope(this, scope) } // focus here 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 {<!-- --> // This this is an instance of the ReactiveEffect class, mount the currently activated effect to the parent of the current instance this.parent = activeEffect // At this point, activeEffect will point to the current instance, where the mounted fn function is the first callback parameter of effect in our business code activeEffect = this shouldTrack = true trackOpBit = 1 << + + effectTrackDepth if (effectTrackDepth <= maxMarkerBits) {<!-- --> initDepMarkers(this) } else {<!-- --> cleanupEffect(this) } // Note that the return here is the arrow function of the callback parameter in our own business code. Once this function is executed, it will trigger the getter behavior of the proxy object // It will trigger the above get property, which is the createGetter method return this.fn() } finally {<!-- --> if (effectTrackDepth <= maxMarkerBits) {<!-- --> finalizeDepMarkers(this) } trackOpBit = 1 << --effectTrackDepth activeEffect = this. parent shouldTrack = lastShouldTrack this.parent = undefined if (this. deferStop) {<!-- --> this. stop() } } } stop() {<!-- --> // stopped while running itself - defer the cleanup if (activeEffect === this) {<!-- --> this. deferStop = true } else if (this. active) {<!-- --> cleanupEffect(this) if (this.onStop) {<!-- --> this. onStop() } this.active = false } } }
-
Review a few things that effect does
- Generate a ReactiveEffect instance
- Trigger the fn method, thus activating the getter
- Establish a link between targetMap and activeEffect
dep.add(activeEffect)
activeEffect.deps.push(dep)
-
To sum up, based on the business module and the following process, we sorted out the reactive responsive core code
- reactive function
- effect function
- trigger setter
-
In fact, we can simplify the above process to
- create proxy
- Collect effect dependencies
- Dependencies that trigger collection
Deleted core implementation code
We split it into three parts: rollup build program and configuration, source code package and sample program
1) rollup construction program
rollup.config.js
import resolve from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' import typescript from '@rollup/plugin-typescript' export default [ {<!-- --> // entry file input: 'packages/vue/src/index.ts', // packaged export output: [ // For example: commonjs, esm, etc., we don't need to support so many // only need to export packages in iife mode {<!-- --> // open sourceMap sourcemap: true, // export file address file: './packages/vue/dist/vue.js', // Generate the format of the package format: 'iife', // variable name name: 'Vue' } ], // handle plugins plugins: [ //ts typescript({<!-- --> sourceMap: true }), // path completion for module import resolve(), // Convert commonjs to ESM commonjs() ] } ]
package.json configuration
"type": "module", "scripts": {<!-- --> "dev": "rollup -c -w", "build": "rollup -c" }
2) Source package
2.1 The directory structure is as follows
- packages
- reactivity
- src
- baseHandlers.ts
- dep.ts
- effect.ts
- index.ts
- reactive.ts
- src
- vue
- dist
- src
- index.ts
- examples
- reactive.html
- reactivity
2.2 The core implementation of the reactivity module package
baseHandlers.ts core implementation
import {<!-- --> track, trigger } from './effect' const get = createGetter() const set = createSetter() export const mutableHandlers: ProxyHandler<object> = {<!-- --> get, set } function createGetter() {<!-- --> return function get(target: object, key: string | symbol, receiver: object) {<!-- --> // trigger get const res = Reflect. get(target, key, receiver) // Corresponding to dependency collection and triggering: every time get is triggered, the function that triggers the getter behavior should be collected so that when the setter behavior is triggered, the corresponding function is called track(target, key) return res } } function createSetter() {<!-- --> return function set( target: object, key: string | symbol, value: unknown, receiver: object ) {<!-- --> const res = Reflect.set(target, key, value, receiver) // In the setter behavior, trigger the dependency trigger(target, key, value) return res } }
dep.ts core implementation
import {<!-- --> ReactiveEffect } from './effect' export type Dep = Set<ReactiveEffect> export const createDep = (effects?: ReactiveEffect[]): Dep => {<!-- --> const dep = new Set<ReactiveEffect>(effects) as Dep return dep }
effect.ts core implementation
import {<!-- --> Dep, createDep } from './dep' export function effect<T = any>( fn: () => T) {<!-- --> const _effect = new ReactiveEffect(fn) _effect. run() } export let activeEffect: ReactiveEffect | undefined export class ReactiveEffect<T = any> {<!-- --> constructor(public fn: () => T) {<!-- -->} run() {<!-- --> activeEffect = this // Mount the current activeEffect variable return this.fn() } } type KeyToDepMap = Map<any, Dep> const targetMap = new WeakMap<any, KeyToDepMap>() export function track(target: object, key: unknown) {<!-- --> // console.log('track: collect dependencies') if (!activeEffect) return let depsMap = targetMap. get(target) if (!depsMap) {<!-- --> targetMap.set(target, (depsMap = new Map())) } // Build relationships let dep = depsMap. get(key) if (!dep) {<!-- --> depsMap.set(key, (dep = createDep())) } trackEffects(dep) // depsMap.set(key, activeEffect) // console. log(targetMap) } // Use dep to track all effects of the specified key in turn export function trackEffects(dep: Dep) {<!-- --> dep. add(activeEffect!) } // Handle the trigger logic in trigger export function trigger(target: object, key: unknown, newValue: unknown) {<!-- --> // console.log('trigger: trigger dependency') const depsMap = targetMap. get(target) if (!depsMap) {<!-- --> return } const dep: Dep | undefined = depsMap.get(key) if (!dep) {<!-- --> return } triggerEffects(dep) } export function triggerEffects(dep: Dep) {<!-- --> const effects = Array.isArray(dep) ? dep : [...dep] // Trigger the loop sequentially for (const effect of effects) {<!-- --> triggerEffect(effect) } } // Trigger specified dependencies export function triggerEffect(effect: ReactiveEffect) {<!-- --> effect. run() }
index.ts core implementation
export {<!-- --> reactive } from './reactive' export {<!-- --> effect } from './effect'
reactive.ts core implementation
import {<!-- --> mutableHandlers } from './baseHandlers' export const reactiveMap = new WeakMap<object, any>() export function reactive (target: object) {<!-- --> return createReactiveObject(target, mutableHandlers, reactiveMap) } function createReactiveObject( target: object, baseHandlers: ProxyHandler<any>, proxyMap: WeakMap<object, any> ) {<!-- --> const existingProxy = proxyMap. get(target) if (existingProxy) {<!-- --> return existingProxy } const proxy = new Proxy(target, baseHandlers) proxyMap.set(target, proxy) return proxy }
2.3 The core implementation of the vue module
Location: vue/src/index.ts
export {<!-- --> reactive, effect } from '@vue/reactivity'
3) Sample program
Location: vue/examples/reactive.html
<script src="../dist/vue.js"></script> <body> <div id='app'> <p id='p1'></p> <p id='p2'></p> </div> </body> <script> const {<!-- --> reactive, effect } = Vue // console. log(reactive) const obj = reactive({<!-- --> name: 'Zhang San' }) // console.log(obj.name) // trigger getter // obj.name = 'Li Si' // trigger setter effect(()=>{<!-- --> document.querySelector('#p1').innerText = obj.name // A getter behavior must be exposed here, so that we can complete the collection of dependencies }) effect(()=>{<!-- --> document.querySelector('#p2').innerText = obj.name // A getter behavior must be exposed here, so that we can complete the collection of dependencies }) setTimeout(() => {<!-- --> obj.name = 'Lee Si' }, 2000) </script>