Vue3 source code combing: Responsive core implementation of reactive and effect functions

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
          }
        }
        
  • 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
  • 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
          }
        }
        
  • 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
    • vue
      • dist
      • src
        • index.ts
      • examples
        • reactive.html

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>