advantages

Reactive has been completely rewritten in VUe3.0, although the design idea is basically the same as vue2, except that Object. DefineProperty is rewritten as es6 proxy, but it also brings the following benefits

  • You can listen for additions and deletions of objects without additional API support
  • You can listen for array changes without additional rewriting of push, splice, and so on
  • Supports monitoring of map, SET, WeakMap, and WeakSet set types
  • Lazy recursion is used. Vue2 uses a forced recursive approach to listen on nested objects. Vue3, on the other hand, creates a proxy for a nested object that is read inside the object

The principle of analytic

Reactive, track, trigger and effect are mainly analyzed as follows

For details about proxy, see Ruan Yifeng Proxy

reactive

Create reactive objects and configure set, GET, has, and so on for them.

Due to the space problem, only the key codes are retained here, and some verification codes are removed.

All the code in: vue – next/packages/reactivity/SRC/reactive. Ts

function reactive(target: object) {
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
} 
function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler
       
        , collectionHandlers: ProxyHandler
        
       ) {
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
Copy the code

CollectionHandlers are the agent for collections (Map,Set,WeakMap,WeakSet), and baseHandlers are the agent for objects and arrays. Here we focus on Set and GET in baseHandlers

get

//vue-next/packages/reactivity/src/baseHandlers.ts

function get(target: Target, key: string | symbol, receiver: object) {
    const targetIsArray = isArray(target)
    ArrayInstrumentations (); arrayInstrumentations ();
    //1. If ['includes', 'indexOf', 'lastIndexOf'] is used to track each item in the array.
    //2. If it is ['push', 'pop', 'shift', 'unshift', 'splice'], track will be paused during execution because push will trigger set twice, once to set value and once to change length. May lead to infinite recursion [detailed reference this issue] (https://github.com/vuejs/vue-next/issues/2137)
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }
    // Effect is collected here
    if(! isReadonly) { track(target, TrackOpTypes.GET, key) }if (shallow) {
      return res
    }
    
    //unref processing, returns the actual value of ref
    if (isRef(res)) {
      constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
    }
    // this is a lazy recursion
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
Copy the code

set

//vue-next/packages/reactivity/src/baseHandlers.ts

function set(target: object, key: string | symbol, value: unknown, receiver: object) :boolean {
    const oldValue = (target as any)[key]
 
    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)) {
    // This triggers an update
      if(! hadKey) {// If it is a new key, add
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
      // Otherwise it is modified
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
Copy the code

track

Dependencies are collected, stored in a global map object, and triggered on GET.

Format for:


{
    target: {
        key: [effect1, effect1, ...] }}Copy the code

Where activeEffect is assigned when effect creates the side effect function.

//vue-next/packages/reactivity/src/effect.ts

const targetMap = new WeakMap<any, KeyToDepMap>()

function track(target: object, type: TrackOpTypes, key: unknown) {
  if(! shouldTrack || activeEffect ===undefined) {
    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 =new Set()))}if(! dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) } }Copy the code

trigger

//vue-next/packages/reactivity/src/effect.ts

function trigger(target: object, type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
// Get the corresponding dependency
  const depsMap = targetMap.get(target)
  if(! depsMap) {// never been tracked
    return
  }

  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }if (type === TriggerOpTypes.CLEAR) {
    // If it is cleared, all dependencies corresponding to the key are updated
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // If it is length, only the code has dependencies on arr.length or those beyond the new length that need to be updated.
    // For example: oldArr: [1, 2, 3] oldArr. Length = 2; Values that depend on 3 need to be updated
    depsMap.forEach((dep, key) = > {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
     // Add a dependency for the key
    if(key ! = =void 0) {
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if(! isArray(target)) {// Here is the dependency to get the collection
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // if oldArr: [1,2,3] oldArr[3] = 4, the legth dependency is triggered
          add(depsMap.get('length'))}break
      case TriggerOpTypes.DELETE:
        if(! isArray(target)) {// Get the dependency of the collection
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
      // Get the dependency of the collection
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break}}Vue3's dependency updates are microtasks, and they are added to determine if there are any identical dependencies in the queue. (Mainly in run-time core/ SRC /scheduler.ts)
  const run = (effect: ReactiveEffect) = > {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)
}
Copy the code

effect

Effect is the core of the connection to the above methods. This is where all the responses start to be injected.

Let’s take a look at the code implementation:

//vue-next/packages/reactivity/src/effect.ts

function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  // Computed is lazy and does not fire immediately, but only when it gets the corresponding value
  if(! options.lazy) { effect() }return effect
}

function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
  const effect = function reactiveEffect() :unknown {
    if(! effect.active) {return options.scheduler ? undefined : fn()
    }
    if(! effectStack.includes(effect)) {// Effect deps is removed to prevent repeated caching
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        // Cache activeEffect, store depTarget on get
        activeEffect = effect
        // Execute the injected render function
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]}}}asReactiveEffect effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect =true
  effect.active = true
  effect.raw = fn
  // Two-way cache is used for debugging.
  effect.deps = []
  effect.options = options
  return effect
}
Copy the code

During vUE rendering

instance.update = effect(function componentEffect() {...// In this case, the template will get the value of reactive, ref and other reactive packages, so as to collect the effect wrapped by componentEffect. When data is reset, set will be triggered to update the effect and update the component
    const subTree = (instance.subTree = renderComponentRoot(instance))
    ...
}, {
  scheduler: queueJob,
  allowRecurse: true
})
Copy the code