Original link: www.yingpengsha.com/vue3-0-xian…

preface

The overall responsiveness of Vue3.0 and Vue2.0 has not changed, but the implementation details have changed significantly. Vue3.0 decouples reactive systems from the body of the code, which means that Vue3.0’s reactive systems can be used as a separate library, just like RxJS. This article is designed to deepen the understanding of the responsivity principle of Vue3.0.

Design ideas

Although the responsive thinking of Vue3.0 and Vue2.0 has not changed, for the sake of review and explanation, we will reorganize the responsive system design of Vue

What is reactive

When object A changes, object B also changes

  • Reactive programming: if the value of b or C changes, the value of A changes accordingly (a := b + c).
  • Responsive layout: As the view window changes, so does the layout of elements within the view
  • MVVM: As the Model changes, so does the View

Object A in Vue is the data, and object B is the render function of the view or Watch or computed

What does Vue’s responsive system need to do

  1. How do you know if the data has changed
  2. How do you know what data the response object depends on and establish dependencies
  3. How do I notify dependent response objects to respond when data changes

We then translate the above requirements into technical terms that we’ve all heard more or less before

  1. The data held
  2. Depend on the collection
  3. Distributed update

Realize the principle of

The data held

How do you know if the data has changed

An overview of the

How do you know that the data has changed, and how do you know that the data has been manipulated? The operations include (add, delete, modify, search, etc.). Vue2.0 uses object.defineProperty to hijack and customize setter and getter operations for objects, but there are some problems with this:

  • The data defineProperty of the array data type cannot be hijacked directly, so it needs to be done by comparing hacks
  • increase,deleteOperations cannot be captured in some scenarios, and in some scenarios cannot be hijacked, we must use them$set,$deleteThese VUe-wrapped functions replace JS’s native value operations, increasing the mental cost
  • Waste of performanceSince there is no way to know which values need to be responsive, Vue2.0 will hold anything that can be held hostage in data, but in practice developers tend to change data in a much more granular manner, so this can result in a certain amount of wasted performanceObject.freeze()Such operations can solve this problem to some extent.)

Vue3.0 uses Proxy to monitor data changes. By definition, Proxy is perfect for the purpose of data hijacking. Please refer to MDN introduction:

Proxy objects are used to create a Proxy for an object to intercept and customize basic operations (such as property lookup, assignment, enumeration, function calls, and so on).

In a sense, Proxy can be seen as an enhancement of Object.defineProperty, which has richer content that can be held hostage and solves the problems with using defineProperty described above:

  • There is no need to do special hostage-taking for arrays, the Proxy takes care of it
  • Add and delete operation Proxy can also be directly hijacked
  • Because of some features of proxies, proxies can implement lazy hostage-taking without the need for deep hostage-taking of all values.
  • And because the Proxy does not make changes to the data source, you can ensure that there are not too many side effects

Implementation details

Vue3.0 uses the Composition API to hold data in a responsive manner. We will only talk about reactive(), the most typical

reactive
export function reactive<T extends object> (target: T) :UnwrapNestedRefs<T>
export function reactive(target: object) {// If the target data has beenreadonly(), returns directly without reactive processingif (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  // Call a generic function for reactive processing
  return createReactiveObject(
    target, // Target data
    false.// Whether to perform read-only operations
    mutableHandlers, // Object/Array type of proxy processor
    mutableCollectionHandlers // Map/Set/WeakMap/WeakSet proxy processor)}Copy the code
createReactiveObject
function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>) {
  // If it is not an object, throw an error and return
  if(! isObject(target))return target
  
  // If the object is already responsive, return it directly, but if readonly() is already responsive, do not return it and continue execution
  if(target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]))return target
  
  Readonly and Reactive have one cache each
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  
  // If the object has already been proxied, it is fetched directly from the cache
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  
  // Determine whether the target object is special or does not need to be hijacked, if so, return directly
  Object/Array => targeType.mon, Map/Set/WeakMap/WeakSet => targeType.collection
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  
  WeakMap/WeakSet Creates a Proxy, if the target object type is Map/Set/WeakMap/WeakSet uses a Proxy processor specifically for collections, and vice versa
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  
  // Cache
  proxyMap.set(target, proxy)
  / / return
  return proxy
}
Copy the code
mutableHandlers

Object/Array agent processor

Some constant value judgments in reactive situations, such as readOnly and shadow, are ignored for easy reading

export const mutableHandlers: ProxyHandler<object> = {
  get(target: Target, key: string | symbol, receiver: object) {
    / /... Internal constant proxy
    // ReactiveFlags.IS_REACTIVE = true
    // ReactiveFlags.IS_READONLY = false
    // ReactiveFlags.RAW = target
		
    // Whether the target object is an array
    const targetIsArray = isArray(target);

    // Special handling when calling some specific array methods
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver);
    }

    / / get the value
    const res = Reflect.get(target, key, receiver);

    // If it is some native built-in Symbol, or the value does not need to trace the direct return
    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : isNonTrackableKeys(key)
    ) {
      return res;
    }

    // Rely on collection
    track(target, TrackOpTypes.GET, key);

    // If the value is already Ref(), whether to return a native Ref or its value depends on whether an array is currently accessed by a normal key
    if (isRef(res)) {
      constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key);return shouldUnwrap ? res.value : res;
    }

    // If it is an object, then it is held hostage (lazy-response source)
    if (isObject(res)) {
      return reactive(res);
    }

    // Return the result
    return res;
  },
  set(target: object.key: string | symbol, value: unknown, receiver: object) :boolean {
    / / the old value
    const oldValue = (target as any)[key];
    
    // The new value removes possible responses
    value = toRaw(value);
    
    // If the old value is a Ref value, it is passed to Ref processing
    if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = value;return true;
    }

    // There is no corresponding key value
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);
    
    / / set the value
    const result = Reflect.set(target, key, value, receiver);
    
    // Send updates
    if (target === toRaw(receiver)) {
      if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value); }else if(hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue); }}return result;
  },
  deleteProperty(target: object.key: string | symbol): boolean {
    const hadKey = hasOwn(target, key);
    const oldValue = (target as any)[key];
    const result = Reflect.deleteProperty(target, key);
    // Send updates
    if (result && hadKey) {
      trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
    }
    return result;
  },
  has(target: object.key: string | symbol): boolean {
    const result = Reflect.has(target, key);
    // If the value is not a native internal Symbol value, a dependent collection is performed
    if(! isSymbol(key) || ! builtInSymbols.has(key)) { track(target, TrackOpTypes.HAS, key); }return result;
  },
  ownKeys(target: object) : (string | number | symbol)[] {
  	// Rely on collection
    track(
      target,
      TrackOpTypes.ITERATE,
      isArray(target) ? "length" : ITERATE_KEY
    );
    return Reflect.ownKeys(target); }};Copy the code
collectionHandlers

Some constant value judgments in reactive situations, such as readOnly and shadow, are ignored for easy reading

Map/Set/WeakMap/WeakSet type WeakSet proxy processor Because the above four types of modified values are modified by functions, so the proxy function only intercepts the GET method, which is used to intercept the operation function called by the response object, and then carries out specific dependency collection or update distribution

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) = > {
    / /... Internal constant proxy
    // ReactiveFlags.IS_REACTIVE = true
    // ReactiveFlags.IS_READONLY = false
    // ReactiveFlags.RAW = target
		
    // Use the corresponding encapsulated functions for processing
    return Reflect.get(
      hasOwn(mutableInstrumentations, key) && key in target
        ? mutableInstrumentations
        : target,
      key,
      receiver
    )
  }
}

// Function proxy
const mutableInstrumentations: Record<string.Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key)
  },
  get size() {
    return size((this as unknown) as IterableCollections)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false.false)}/ / get the value
function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // Native objects
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  
  / / primary key
  const rawKey = toRaw(key)
  
  // If it is reactive, the reactive key is dependent collected
  if(key ! == rawKey) { track(rawTarget, TrackOpTypes.GET, key) }// Dependency collection on native keys
  track(rawTarget, TrackOpTypes.GET, rawKey)
    
  // If the target collection has, call HAS for dependency collection, because get() implicitly depends on HAS and returns a responsive key
  const { has } = getProto(rawTarget)
  if (has.call(rawTarget, key)) {
    return toReactive(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
    return toReactive(target.get(rawKey))
  }
}

function size(target: IterableCollections, isReadonly = false) {
  // Rely on collection, key value for Symbol inside Vue ('iterate')
  target = (target as any)[ReactiveFlags.RAW]
  track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.get(target, 'size', target)
}

function add(this: SetTypes, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  // Call native HAS
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  
  // If it does not exist, add it and send an update
  if(! hadKey) { target.add(value) trigger(target, TriggerOpTypes.ADD, value, value) }return this
}

function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const { has, get } = getProto(target)

  // Check whether the corresponding key already exists based on the incoming key and the real rawKey that the key may have
  let hadKey = has.call(target, key)
  if(! hadKey) { key = toRaw(key) hadKey = has.call(target, key) }// Fetch the old value and set it
  const oldValue = get.call(target, key)
  target.set(key, value)
  
  // If it is new, it triggers the new update, and if it is not, it triggers the Settings update
  if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return this
}

function deleteEntry(this: CollectionTypes, key: unknown) {
  const target = toRaw(this)
  const { has, get } = getProto(target)
  
  // For the same set, check whether a corresponding key already exists by checking the incoming key and the real rawKey that the key may have
  let hadKey = has.call(target, key)
  if(! hadKey) { key = toRaw(key) hadKey = has.call(target, key) }// Fetch the old value and delete it
  const oldValue = get ? get.call(target, key) : undefined
  const result = target.delete(key)
  
  // Trigger delete update
  if (hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

function clear(this: IterableCollections) {
  const target = toRaw(this)
  consthadItems = target.size ! = =0
  const result = target.clear()
  // Trigger a clean update
  if (hadItems) {
    trigger(target, TriggerOpTypes.CLEAR, undefined.undefined.undefined)}return result
}

function forEach(
  this: IterableCollections,
  callback: Function, thisArg? : unknown) {
  const observed = this as any
  const target = observed[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
 	// Collect dependencies based on iterators
  track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)  
 	// make the subset responsive
  return target.forEach((value: unknown, key: unknown) = > {
    return callback.call(thisArg, toReactive(value), toReactive(key), observed)
  })
}
Copy the code

Depend on the collection

How do you know what data the response object depends on and establish dependencies

An overview of the

How to know what data the response object depends on is a further question of what data the response object uses. The general idea of Vue is like this. For example, I have a function fnA that uses B and C in data. To know that fnA uses B and C, we simply run fnA, wait for fnA to be acquired in B and C, and then establish a dependency between the two. There are some concepts in Vue2.0: Watcher, Dep, Target.

  • Watcher stands for fnA
  • Dep is an object in the setter for B that holds the Watcher collection
  • Target is Watcher, which is currently doing dependency collection

New Watcher(fnA) => **target = current Watcher and fnA => fnA => fnA Vue3.0 has a different implementation. Because Vue3.0 no longer makes intrusive changes or hijks to data, Vue3.0 has a separate static variable store dependency. This variable is called targetMap and introduces a new concept called effect, which is similar to the **Watcher ** of Vue2.0, but with a shift in concept from listener to side effect, which refers to the side effect that occurs when the corresponding dependency value changes

The data type

The data types are as follows: The targetMap key points to Data, the KeyToDepMap key points to the ‘A’ and ‘B’ keys, and the value is the Watcher set in Effect

type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
Copy the code



Implementation details

Effect

Effect is essentially the Watcher of Vue2.0, but it does a relatively simple job

export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
  // If the function passed in is already an effect, the original function is removed and processed
  if (isEffect(fn)) { fn = fn.raw }
  
  // Create reactive side effects
  const effect = createReactiveEffect(fn, options)
  
  // Computed is lazy if it is not a side effect of laziness and runs directly and relies on collection
  if(! options.lazy) { effect() }return effect
}
Copy the code
createReactiveEffect
function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
  // Returns the wrapped side effect function
  const effect = function reactiveEffect() :unknown {
    // Side effect function core, more on that later
  } as ReactiveEffect
  
  // Some static attribute definitionseffect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect =true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}
Copy the code
reactiveEffect
function reactiveEffect() :unknown {
  // If the side effect is already paused, its scheduler is executed first before the function body is run
  if(! effect.active) {return options.scheduler ? undefined : fn()
  }
  // If the current side effect is not entered at runtime
  if(! effectStack.includes(effect)) {// Clear the old dependencies first
    cleanup(effect)
    try {
      // Enable dependency collection
      enableTracking()
      // Add to the running side effects stack
      effectStack.push(effect)
      // confirm the current side effect, target in Vue2.0
      activeEffect = effect
      // Execute the function
      return fn()
    } finally {
      // Exit the stack
      effectStack.pop()
      // Turn off dependency collection
      resetTracking()
      // Pass the current side effect to the previous one or empty it
      activeEffect = effectStack[effectStack.length - 1]}}}Copy the code
track

Is called in the data-held GET/HAS/ownKeys

export function track(target: object.type: TrackOpTypes, key: unknown) {
  // If no collection is currently in progress, exit
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  // Retrieve the object's KeyToDepMap, if not, create a new one
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Retrieve the dependency set for the corresponding key value, if not, create a new one
  let dep = depsMap.get(key)
  if(! dep) { depsMap.set(key, (dep =new Set()))}// If there is no current queue in the dependency, enter it to prevent repeated dependency setting
  if(! dep.has(activeEffect)) {// Two-way dependency ensures consistency between old and new dependencies
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}
Copy the code

Distributed update

How do I notify dependent response objects to respond when data changes

An overview of the

Distributing updates is the easiest part of the three, but Vue3.0 implements more details than Vue2.0, simply fetching the corresponding set of side effects from the dependency when the value changes and triggering the side effects **. We can see the call to the update trigger in the set/deleteProperty in the data hostage above.

Implementation details

trigger
export function trigger(
  target: object.type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
  // Return the KeyToDepMap of the object to which the response value is located
  const depsMap = targetMap.get(target)
  if(! depsMap) {// never been tracked
    return
  }

  // Set of side effects that need to be triggered
  const effects = new Set<ReactiveEffect>()
  // Functions added to the collection can be seen below when triggered
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    // Pass in a collection of side effects
    if (effectsToAdd) {
      // Iterate over the set of incoming side effects
      effectsToAdd.forEach(effect= > {
        // Add a value to the set of side effects that are about to trigger if the side effect is not currently executing (preventing an endless loop of repeated calls) or allows recursive calls
        if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }if (type === TriggerOpTypes.CLEAR) {
  	// If the corresponding modification operation is, such as new Set().clear()
    // Add all subvalues of the side effects to the side effects queue
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // If you change the length of the array, it means that the values after the new length are changed, and the corresponding side effects of these subscripts are queued
    depsMap.forEach((dep, key) = > {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // Modify value, new value, delete value
    // Add the side effects of the corresponding value to the queue
    if(key ! = =void 0) {
      add(depsMap.get(key))
    }

    // Add/delete corresponds to other side effects that need to be triggered (e.g. length dependent side effects, iterator dependent side effects)
    switch (type) {
      / / new
      case TriggerOpTypes.ADD:
        // Add means the length has changed, firing the side effect function corresponding to the iterator and length
        if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          add(depsMap.get('length'))}break
      case TriggerOpTypes.DELETE:
        // Same as above, because the array delete operation is special, it does not appear
        if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        // a function that depends on iterators (e.g., calling new Map().foreach () equals a variant dependence on set)
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break}}// Call the scheduler first or the side effect itself
  const run = (effect: ReactiveEffect) = > {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)
}
Copy the code