Introduction: This paper is based on Vue 3.0.5 for analysis, mainly for personal learning comb process. It was also the first time I had written a serious article. Correct me if I am wrong.

preface

In the last Vue3 analysis series, we analyzed the mount logic and learned that in fact, in the last step of the component mount setupRenderEffect function will collect the side effects (componentEffect) and execute them. Render the current instance by executing the deputy and generate the final page. So how does Vue update the page by modifying the data? Let’s move on to reactive logic parsing of data.

Don’t say much about the source code

createReactiveEffect

First, the createReactiveEffect function that appeared in the previous parsing is called. Among them, activeEffects need to be noted. Its main use is to mark the current side effects that need to be collected, so that the corresponding dependency objects can be associated during dependency collection

// packages/reactivity/src/effect.ts
// TODO generates effect
function createReactiveEffect<T = any> (
  fn: () => T,  // The side effect function passed in
  options: ReactiveEffectOptions / / configuration items
) :ReactiveEffect<T> {
  const effect = function reactiveEffect() :unknown {
    // If effect is not active, which happens after the stop method in effect is called, then the original method fn is called if the scheduler function was not previously called, otherwise it is returned.
    if(! effect.active) {return options.scheduler ? undefined : fn()
    }
    // What about active effects? Check whether the current effect is in the effectStack. If so, no call is made. This is mainly to avoid repeated calls.
    if(! effectStack.includes(effect)) {// Remove side effects
      cleanup(effect)
      try {
        // To enable tracing, a trace state is stored in the trackStack queue and corresponds to the following effectStack to distinguish which effect needs to be tracked
        enableTracking()
        // Place the current effect in the effectStack
        effectStack.push(effect)
        // Then set activeEffect to the current effect
        activeEffect = effect
        // fn and return a value
        return fn()
      } finally {
        When this is complete, the finally phase pops the current effect, restores the original collective-dependent state, and restores the original activeEffect.
        effectStack.pop()
        // Reset the trace
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect

  effect.id = uid++ // Add id unique effecteffect.allowRecurse = !! options.allowRecurse effect._isEffect =true // is used to indicate whether the method is effect
  effect.active = true // Whether to activate
  effect.raw = fn // The callback method passed in
  effect.deps = [] // Hold the current effect DEP array
  effect.options = options // Create effect is passed options
  return effect
}
Copy the code

Let’s look at how to collect dependencies and trigger corresponding effects.


The data response module reactivity

reactive

// packages/reactivity/src/reactive.ts
export function reactive(target: object) {
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    // Is the object __v_isReadonly read-only yes Read-only returns target without hijacking
    return target
  }
  // The actual operation is in createReactiveObject
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
Copy the code

conclusion

Check whether the current object is IS_READONLY, and return if it is. If not, do the proxy logic.

createReactiveObject

// Omit some DEV environment code
// packages/reactivity/src/reactive.ts
// TODO creates proxy objects
function createReactiveObject(
  target: Target, // The object to be proxied
  isReadonly: boolean, The read-only attribute indicates whether the agent to be created is readable only,
  baseHandlers: ProxyHandler<any>, [Object, Array] [Object, Array]
  collectionHandlers: ProxyHandler<any> // Is a hijacking of the Set type, i.e. [Set, Map, WeakMap, WeakSet].
) {
  if(! isObject(target)) {// Return target if it is not an object
    return target
  }
  if( target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) {// If the target is already proxied, return it directly
    return target
  }
  // We create two types of proxy, one is responsive and the other is read-only
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  // If the incoming target already exists, it is returned directly
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  const targetType = getTargetType(target)
  If the type is invalid, return the object directly
  if (targetType === TargetType.INVALID) {
    return target
  }
  // TODO is the final agent
  WeakMap weakSet Set Map // Object Array // Determine the method to be called according to the status
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // Insert the proxy Map table target => proxy
  proxyMap.set(target, proxy)
  return proxy
}
Copy the code

conclusion

The object to be proxied is judged to check whether it meets the proxied conditions. The final judgment type is the proxy type. Let’s examine the corresponding proxy types

mutableHandlers

The main concern is get() and set() because it is through them that the dependency collection is implemented

// packages/reactivity/src/baseHandlers.ts
The /** * handler.get() method is used to intercept an object's read property operations. * The handler.set() method is the catcher for setting the property value operation. * The handler.deleteProperty() method is used to intercept delete operations on object properties. The handler.has() method is a proxy method for the in operator. * handler ownKeys () method is used to intercept * Object in getOwnPropertyNames () * Object in getOwnPropertySymbols () * Object. The keys () * for... In circulation * /

// The TODO agent listens for status
// Normal listening status
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

// Get has four states in the source code
const get = /*#__PURE__*/ createGetter() // Normal get
const shallowGet = /*#__PURE__*/ createGetter(false.true) // Shallow get
const readonlyGet = /*#__PURE__*/ createGetter(true) // Read-only get
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true.true) // Shallow read-only get


// Data is returned mainly via isReadonly shallow and the target type target and whether the data is propped
// The factory function of get
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // If key is proxied, return the corresponding state! isReadonly
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      If the key is read-only, return the corresponding state isReadonly
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }
    // Whether the target is an array
    const targetIsArray = isArray(target)
    // It is not read-only and is an array with the attributes 'includes', 'indexOf' and 'lastIndexOf'
    if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {// If the getter is specified in the target object, receiver is the this value when the getter is called. Receiver => Proxy Proxy object
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)
    // If the key is Symbol, return the res value directly.
    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }
    // If TODO is not read-only, dependency collection is performed
    if(! isReadonly) {// TODO core track relies on collection
      track(target, TrackOpTypes.GET, key)
    }
    // If shallow get returns data directly, no listening is performed
    if (shallow) {
      return res
    }
    // check if it is Ref
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
    }
    // If it is an object
    if (isObject(res)) {
      // Not read-only continue proxy create read-only continue proxy create proxy
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

// Set has two states in the source code
const set = /*#__PURE__*/ createSetter()  // Normal set
const shallowSet = /*#__PURE__*/ createSetter(true) // Shallow set

// TODO set factory function
function createSetter(shallow = false) {
  return function set(target: object, key: string | symbol, value: unknown, receiver: object) :boolean {
    // Get the value you want to change
    const oldValue = (target as any)[key]
    // Not in shallow mode
    if(! shallow) {// toRaw is a simple function that iterates until the object passed does not exist reactiveFlags. RAW, and the desired RAW data is retrieved (before proxy).
      value = toRaw(value)
      if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true}}// Returns a length comparison if the target is an array
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    if (target === toRaw(receiver)) {
      // TODO core trigger relies on update trigger
      if(! hadKey) {// Field changes are new
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // The length changes to update
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}


Copy the code

conclusion

This is similar to the implementation logic of Vue2. The big change is that Vue2 hijacks the attributes of the Object via Object.defineProperty. Dependencies are collected and triggered within sets and GETS. It is also worth noting that Object.defineProperty can only hijack Object attributes. There is no way to hijack an array. So in Vue2. Rewrite the seven can modify the array method, a push, pop, shift, unshift, splice, sort, reverse. Dependency collection and dependency triggering occur during execution.

Vue3 abandons Vue2’s hijacking of attributes. The object is changed to be represented by Proxy.

  • The Proxy advantages

    • You can listen directly on the entire object rather than the properties.
    • You can listen for changes in the array directly.
    • There are 13 interception methods, such asownKeys,deleteProperty,hasIs such asObject.definePropertyDo not have.
    • It returns a new Object, so we can just manipulate the new Object for our purposes, whereas Object.defineProperty can only be modified by iterating through the Object attributes.
    • As a result of the new standard, browser manufacturers will continue to focus on performance optimization, known as the performance bonus of the new standard.
  • The Proxy shortcomings

    • Browser compatibility issues and can’t be smoothed with Polyfill.
  • Object. DefineProperty advantage

    • It has good compatibility and supports IE9, but Proxy has browser compatibility problems and cannot be smoothed by Polyfill.
  • Object. DefineProperty faults

    • Only properties of objects can be hijacked, so we need to traverse each property of each object.
    • Do not listen on arrays. The array is listened on by overriding the seven methods that change the data.
    • Nor is it new to es6Map , SetThese data structures do the listening.
    • Can also not listen to add and delete operations, throughVue.set()Vue.deleteTo implement responsive.

track

Dependency collection and dependency mapping stage

// packages/reactivity/src/effect.ts
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // Receive three parameters target Proxy object type Trace type key Trigger proxy object key
  // Determine that there is no current pause trace and no active side effects
  // Remember activeEffect. This is an effect that is assigned when a side effect is created
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  // targetMap ===> weakMap weak reference to see if the current proxy object is already proxied
  let depsMap = targetMap.get(target)
  // If not, store it in weakMap object to avoid repeated proxy
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// depsMap===> Map object
  let dep = depsMap.get(key)
  // Checks if properties on the current target are proxied to avoid double proxiing
  if(! dep) {// dep is a Set of unique values in the deP
    depsMap.set(key, (dep = new Set()))}// if the set dependent on the key also has no activeEffect, add the activeEffect to the set and insert the current set into the deps array of the activeEffect
  if(! dep.has(activeEffect)) { dep.add(activeEffect)// Deps is the set array for the key dependent in effect
    activeEffect.deps.push(dep)
  }
}
Copy the code

trigger

export function trigger(
  target: object, // Trigger the object
  type: TriggerOpTypes, // type Indicates the trigger typekey? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  // If depsMap is empty, the dependency is not traced
  if(! depsMap) {return
  }
  // Side effect mapping table
  const effects = new Set<ReactiveEffect>()

  // The main effect is to add the side effects of each change key to the effects mapping table. Form an execution stack. Because it's Set. The only value
  // TODO so if multiple properties are modified under the same component. This is because the Set type actually performs the effect execution to replay the last time. Clever solution to multiple effect triggers and repeated rendering
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }// If it is clear, all effects of the object are triggered.
  if (type === TriggerOpTypes.CLEAR) {
    // depsMap stores all attributes of the current target and their corresponding side effects => key:effect
    // passed to the add method
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // If key is length and target is array, effects with key is length and key is greater than or equal to the new length are triggered because the array length is changed.
    depsMap.forEach((dep, key) = > {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // Get the side effects of the key
    if(key ! = =void 0) {
      add(depsMap.get(key))
    }
    // Add modify delete
    switch (type) {
      case TriggerOpTypes.ADD:
        / / add
        if(! isArray(target)) {// Not an array
          // packages/reactivity/src/collectionHandlers.ts
          Size createForEach createIterableMethod TODO overwrites several methods that intercept length related methods
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            // packages/reactivity/src/collectionHandlers.ts
            // TODO createIterableMethod
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // Yes array key is an integer. When changing the length
          add(depsMap.get('length'))}break
      case TriggerOpTypes.DELETE:
        / / delete
        // All in the DEV environment
        if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        / / set
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break}}const run = (effect: ReactiveEffect) = > {
    // effect. Options. Scheduler is used to run effect if a scheduler function is passed in.
    // However, effect may not be used in scheduler, such as computed, because computed runs effect lazily
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  Effects.foreach (run) => effects.foreach (effect => run(effect))
  // Perform all side effects
  effects.forEach(run)
}

Copy the code

conclusion

The function of track and trigger is to establish the dependency relationship so that the corresponding side effect function in the mapping table can be executed during attribute modification, and then the Patch method of VNode can be triggered. The DOM is updated and the page is finally updated.

The flow chart

Add to that flow chart I’ve been talking about.

conclusion

Vue3 the entire component initialization (mount) process has been covered. The component update process will be supplemented later.


Previous articles:

Vue3 parse series createAppAPI function

Vue3 parse series of mount functions