Vue3.2 responsive principle source code analysis, and the difference between vue2. X responsive

The source code version of this article is Vue3.2.11. Here is the Vue2 responsive source code analysis point Analysis of Vue2 responsive principle source code

We know that compared with Vue2.x's responsive Vue3, the entire responsive Vue3 has been significantly upgraded; Then Vue3.2 has made many changes compared with version 3.0. Let's have a look

Difference between Vue3 and Vue2 responsive

Responsive performance improvement

According to the release of Youda on August 10 Vue3.2 original description hear:

  • More efficient ref implementation, with a read increase of about 260% and a write increase of about 50%
  • Increase collection speed by about 40%
  • Reduce memory consumption by about 17%

Differences in use

In Vue2, as long as the properties written in the object returned by the data function in the component are automatically responsive

Vue3 defines common type responsive data through ref and complex type responsive data through reactive

<script setup>
  import { ref, reactive, toRefs } from "vue"
  const name = ref('Mu Hua')
  const obj = reactive({ name: 'Mu Hua' })
  const data = { ...toRefs(obj) }
</script>

Extension: you can turn a responsive object into a normal object through toRefs
When the reactive object defined by reactive is deconstructed (expanded) or destroyed, the response will become invalid. Because there are many attributes under the reactive instance, the deconstruction will be lost. Therefore, toRefs can be used when it is necessary to deconstruct and maintain the response

Differences in source directory structure

The core part of the Vue2 responsive source code is in the src/core/observer directory, but it also introduces many other directories, which are not independent and have a high degree of coupling

Vue3 responsive source code is all in the packages/reactivity directory. It does not involve anywhere else. It has independent functions, and is published as an npm package separately, which can be integrated into other frameworks

Principle difference

We know that using Object.defineProperty to implement responsive objects in Vue2 has some defects

  • Based on attribute interception, all attributes will be recursive during initialization, which has a certain impact on performance. Moreover, the response cannot be triggered for new attributes added to the object. The corresponding solution is to add new attributes through the Vue.set() method
  • The internal changes of the array cannot be detected. The corresponding solution is to rewrite 7 methods that will change the original array

In Vue3, Proxy is used for reconstruction, which completely replaces defineProperty, because Proxy can hijack the whole object, so the above problem does not exist

So how to solve these problems?

object

Let's take a look at the response processing of Vue2 when rendering for the first time. The source code address is line src/core/observer/index.js - 157

...
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () { ... },
    set: function reactiveSetter (newVal) { ... }
})
...

It can be seen from the parameters that it needs to find obj[key] in obj according to the specific key to intercept. Therefore, it is necessary to meet a precondition. You must know what the key is at the beginning, so you need to traverse each key and define getter s and setter s. This is also why the attributes added later have no response

This is the case in Vue3

// Ref source code ` packages / reactivity / ref.ts - line 142`
// Reactive source code ` packages / reactivity / reactive.ts - line 173`
new Proxy(target,{  // target is the object returned by the data of the component
  get(target, key){},
  set(target, key, value){}
})

It can also be seen from the parameters that when you start creating a response, you don't need to know what fields are in the object at all, because you don't need to pass a specific key, so even if it is added later, it can be intercepted naturally

That is to say, it will not recursively traverse, and set all the used and unused responses, so as to speed up the first rendering

array

In Vue2

  • One reason is that the api Object.defineProperty cannot listen for changes in array length
  • Second, because the array length may be very long, for example, lenth is thousands or tens of thousands, especially considering the performance consumption and user experience, Vue is designed not to listen to the operation of modifying array elements directly through subscripts

Extending a question, why can't you listen to the change of array length? Look at the picture first

As shown in the figure, the corresponding value can be changed only when configurable is true, which can also be understood as being monitored. Length itself cannot be monitored, so it cannot be monitored when the array length is changed

If you forcibly change its configurable to true, an error will be reported, because major browser manufacturers and JS engines do not allow you to modify the configurable of length, which is the case, so there is such code in the source code

// `Src / core / observer / index.js - line 144`
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
    return
}

Therefore, in order to better operate the array and trigger the response, we rewrite the seven methods that will change the original array, and then manually send the update through ob.dep.notify(). The source address is src/core/observer/array.js

Proxy is used in Vue3. Proxy means proxy. Review the syntax

new Proxy(target,{
  get(target, key){},
  set(target, key, value){}
})

according to Description of Proxy in MDN That's true

  • Target: object virtualized by Proxy. It is often used as the storage back end of agents. Validate invariants (invariant semantics) about the object's non extensibility or non configurable attributes according to the target

Note: the length of the array is a non configurable property, so the Proxy can naturally monitor the length of the array

Differences in dependency collection

Vue2 implements dependency collection through three classes: Observer, Dep and Watcher. See my other article for the detailed process Analysis of Vue responsive principle source code

Vue3 collects dependencies through track and triggers updates through trigger. In essence, it is implemented with WeakMap, Map and Set. See the implementation process of the following source code for details

Defect difference

It is also mentioned above that defineProperty in Vue2 cannot monitor the internal changes of the new object property / array, and if the property value is an object, observe() will be called repeatedly for recursive traversal. In addition, it will set monitoring for all data properties, including unused properties, so the performance is naturally not so good

Vue3 mainly uses a lot of new features of Es6 +, which is not so compatible with old browsers

Vue3 responsive source code analysis

Let's take a look at several flag s defined in Vue3 to mark the type of target object. The following are the enumerated properties

Source address: packages / reactivity / reactive.ts - line 16

export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  RAW = '__v_raw'
}
export interface Target {
  [ReactiveFlags.SKIP]?: boolean // Non responsive data
  [ReactiveFlags.IS_REACTIVE]?: boolean // Is the target responsive
  [ReactiveFlags.IS_READONLY]?: boolean // Is the target read-only
  [ReactiveFlags.RAW]?: any // Represents the source data corresponding to the proxy. This attribute is available when the target is already a proxy object
}

Then start parsing one by one

reactive()

Source address: packages / reactivity / SRC / reactive.ts - line 87

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // If the target is a read-only object, it is returned directly
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target, // You need to create a responsive target object data
    false, // Is not a read-only type
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap // const reactiveMap = new WeakMap<Target, any>()
  )
}

The code here is very simple, mainly calling createReactiveObject()

createReactiveObject()

Source address: packages / reactivity / SRC / reactive.ts - line 173

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // typeof is not of object type, and is returned directly
  if (!isObject(target)) {
    if (__DEV__) console.warn(`value cannot be made reactive: ${String(target)}`)
    return target
  }
  // If it is already responsive, it will be returned directly
  if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
    return target
  }
  // If it already exists in the map, it will be returned directly
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // Return directly without responding
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // Convert target to proxy
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // Add to map
  proxyMap.set(target, proxy)
  return proxy
}

After knowing about what needs to be done in this method, we need to understand the parameters passed in first

The parameter configuration definition is as follows

const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()
export const mutableHandlers: ProxyHandler<object> = {
  get, // get attribute
  set, // modify attribute
  deleteProperty, // Delete attribute
  has, // Do you have a property
  ownKeys // Collect keys, including symbol type or non enumerable keys
}

Here, get, has and ownKeys will trigger the dependency collection track()
set and deleteProperty will trigger the update trigger()

Two important methods are createGetter and createSetter corresponding to get and set

createGetter()

Source address: packages / reactivity / SRC / basehandlers.ts - line 80

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // Access the corresponding tag bit
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      // The receiver points to the caller. The judgment here is to ensure that the proxy itself rather than the successor of the proxy triggers the interception handle
      // There are two ways to trigger the block: one is to access the properties of the proxy object itself, and the other is to access the properties of the object with the proxy object on the object prototype chain, because the query will look down along the prototype chain
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow ? shallowReadonlyMap : readonlyMap
          : shallow ? shallowReactiveMap : reactiveMap
        ).get(target)
    ) {
      // Returns the target itself, which is the original value of the responsive object
      return target
    }
    // Is it an array
    const targetIsArray = isArray(target)
    // It is not a read-only type & & it is an array & & it triggers the methods in the arrayInstrumentations toolset
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // Through proxy call, this of arrayInstrumentations[key] must point to proxy
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // proxy pre return value
    const res = Reflect.get(target, key, receiver)
    // key is symbol or accessed by__ proto__ Property does not do dependency collection and recursive response processing, but directly returns the result
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
    // Collect dependencies on target s that are not read-only. Because the read-only type will not change, the setter cannot be triggered, and the update will be triggered
    if (!isReadonly) {
      // Collect dependencies and store them in the corresponding global warehouse
      track(target, TrackOpTypes.GET, key)
    }
    // For shallow comparison, recursive conversion is not performed, that is, if the object has attribute values or objects, reactive() is not called recursively
    if (shallow) {
      return res
    }
    // The accessed property is already a ref object
    if (isRef(res)) {
      // Return ref.value except array
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }
    // Because proxy can only proxy one layer, if the child element is an object, you need to continue the proxy recursively
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

track() relies on the collection to be put later, together with the distribution of updates

createSetter()

Source address: packages / reactivity / SRC / basehandlers.ts - line 80

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (!shallow) {
      // Take the original value of the new value and the old value, because the newly transmitted value may be responsive data. It is meaningless to directly compare it with the original value on the target
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      // Not array & & old value is ref & & new value is not ref, update ref.value to new value
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
    // Get key value
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // Assignment, equivalent to target[key] = value
    const result = Reflect.set(target, key, value, receiver)
    // Only when the receiver is a proxy instance, it sends updates to prevent the interceptor from triggering updates through the prototype chain
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // If the target does not have a key, it means a new key is added
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // If the old and new values are not equal
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

trigger() sends updates later

There is a question: why use Reflect.get() and Reflect.set() instead of using target[key]?

according to MDN describes that set() returns a Boolean value For example, Reflect.set() will return a Boolean value indicating whether the modification was successful. It will directly assign target[key] = newValue, and an error will be reported if it does not return true. Moreover, no matter how the Proxy modifies the default behavior, you can obtain the default behavior through Reflect. get() similarly

Next is the core content related to the collection and distribution of updates. The relevant codes are all in the effect.ts file, which mainly deals with some side effects. The main contents are as follows:

  • Create effect entry function
  • track dependency collection
  • trigger distribution update
  • cleanupEffect clear effect
  • Stop stop effect
  • trackStack collects pausetracking, enabletracking, and resettracking of the stack

Let's start with the entry function

effect()

Source address: packages / reactivity / SRC / effect.ts - line 145

The main purpose here is to expose a method to create an effect

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // If it is already an effect function, take the original function directly
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // Create effect
  const _effect = new ReactiveEffect(fn)
  if (options) {
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  // If lazy is not true, execute effect directly once. The lazy of the calculated property is true
  if (!options || !options.lazy) {
    _effect.run()
  }
  // return
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

It can be seen that the main thing is to create a new ReactiveEffect in effect_ Effect instance, and the last runner method returned by the function points to the run method in ReactiveEffect

This shows that when the side effect function effect method is executed, the run method is actually executed

So we need to know what is done in the ReactiveEffect and the run method it returns

Let's keep watching

ReactiveEffect

Source address: packages / reactivity / SRC / effect.ts - line 53

The main thing to do here is to use the stack data structure effectstack to debug the execution of the effect before dependency collection, so as to ensure the highest priority of the current effect and clear the memory of the collected dependencies in time

It should be noted that the fn() function will be executed after the tag is completed. This fn function is the function closed by the side effect function. If it is in component rendering, fn is the component rendering function. When it is executed, it will access the data, trigger the getter of target[key], and then trigger the track for dependency collection, which is the dependency collection process of Vue3

// Temporary storage response function
const effectStack: ReactiveEffect[] = []
// Dependency collection stack
const trackStack: boolean[] = []
// Maximum nesting depth
const maxMarkerBits = 30
export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  computed?: boolean
  allowRecurse?: boolean
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope | null
  ) {
    // The related processing of effectScope is in another file, which is not expanded here
    recordEffectScope(this, scope)
  }
  run() {
    if (!this.active) {
      return this.fn()
    }
    // If there is no current effect in the stack
    if (!effectStack.includes(this)) {
      try {
        // activeEffect indicates the effect currently being processed by the dependency collection system
        // First, set the current effect as the globally activated effect, and collect the effects held by the activeEffect in the getter
        // Then put it on the stack
        effectStack.push((activeEffect = this))
        // Resume dependency collection because dependency collection is suspended during the setup function itself
        enableTracking()
        // Record recursion depth bits
        trackOpBit = 1 << ++effectTrackDepth
        // If the number of nested layers of effect does not exceed 30, it is generally not exceeded
        if (effectTrackDepth <= maxMarkerBits) {
          // Marking dependencies is traversal_ deps attribute in the effect instance, and mark the w attribute of each dep as the value of trackOpBit
          initDepMarkers(this)
        } else {
          // The dependency related to the current effect will be cleared if the current effect is exceeded. Normally, it will not be cleared
          cleanupEffect(this)
        }
        // When executing the effect function, such as accessing target[key], the getter will be triggered
        return this.fn()
      } finally {
        if (effectTrackDepth <= maxMarkerBits) {
          // Complete dependency tag
          finalizeDepMarkers(this)
        }
        // Restore to previous level
        trackOpBit = 1 << --effectTrackDepth
        // Reset dependency collection status
        resetTracking()
        // Out of stack
        effectStack.pop()
        // Get stack length
        const n = effectStack.length
        // Point the current activeEffect to the last effect on the stack
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }
  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

track()

Source address: packages / reactivity / SRC / effect.ts - line 188

track is a dependency collector, which is responsible for collecting dependencies and putting them into a dependency management center

// targetMap is the dependency management center, which is used to store the mapping relationship among responsive functions, target objects and keys
// Equivalent to this
// targetMap(weakmap)={
//    target1(map):{
//      key1(dep):[effect1,effect2]
//      key2(dep):[effect1,effect2]
//    }
// }
// Create a map for each target, and each key corresponds to a dep
// dep is used to collect dependent functions, monitor the change of key value, and trigger the dependent functions in dep
const targetMap = new WeakMap<any, KeyToDepMap>()
export function isTracking() {
  return shouldTrack && activeEffect !== undefined
}
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // If no effect is currently activated, no collection is required
  if (!isTracking()) {
    return
  }
  // Get target from dependency management center
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // If not, create one
    targetMap.set(target, (depsMap = new Map()))
  }
  // Get the dep set corresponding to the key
  let dep = depsMap.get(key)
  if (!dep) {
    // Create without
    depsMap.set(key, (dep = createDep()))
  }
  // Development environment and non development environment
  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined
  trackEffects(dep, eventInfo)
}

trackEffects()

Source address: packages / reactivity / SRC / effect.ts - line 212

Here, the currently active effect is collected into the corresponding effect set, that is, dep

Here's a look at two identifiers

dep.n: n is the abbreviation of newTracked, indicating whether it is the latest collection (whether it is the current layer)
dep.w: W is the abbreviation of wasTracked, indicating whether it has been collected to avoid repeated collection

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  // If the number of nested layers of effect does not exceed 30, as mentioned above
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      // Mark new dependency
      dep.n |= trackOpBit
      // Dependencies that have been collected do not need to be collected repeatedly
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Switch to clear dependency mode when it exceeds
    shouldTrack = !dep.has(activeEffect!)
  }
  // If you can collect
  if (shouldTrack) {
    // Collect the currently active effect as a dependency
    dep.add(activeEffect!)
    // The currently active effect collects the dep collection
    activeEffect!.deps.push(dep)
    // Trigger onTrack event in development environment
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}

trigger()

Source address: packages / reactivity / SRC / effect.ts - line 243

Trigger is the trigger corresponding to the dependency collected by track, that is, it is responsible for obtaining the responsive function according to the mapping relationship, and then sending a notice to trigger effects to update

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // Get dependencies from dependency management center
  const depsMap = targetMap.get(target)
  // Dependencies that have not been collected are returned directly
  if (!depsMap) {
    return
  }

  let deps: (Dep | undefined)[] = []
  // The type passed in when trigger is triggered is clear type
  if (type === TriggerOpTypes.CLEAR) {
    // Add all associated dependencies to the queue and prepare to clear
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    // If it is an array type and the length of the array changes
    depsMap.forEach((dep, key) => {
      // If the array length becomes shorter, you need to do the effects and trigger of the deleted array elements
      // That is, the effects corresponding to the elements with index number > = the latest len gt h of the array should be added to the queue to be cleared
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // If the key is not undefined, add corresponding dependencies to the queue, such as adding, modifying, and deleting
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }
    // Add, modify and delete are handled separately
    switch (type) {
      case TriggerOpTypes.ADD: // newly added
        ...
        break
      case TriggerOpTypes.DELETE: // delete
        ...
        break
      case TriggerOpTypes.SET: // modify
        ...
        break
    }
  }
  // Here you get targetMap[target][key] and coexist in deps
  // Next, take out the corresponding effect and call trigger effects to execute

  // Judge the development environment and pass in eventInfo
  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined

  if (deps.length === 1) {
    if (deps[0]) {
      if (__DEV__) {
        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))
    }
  }
}

triggerEffects()

Source address: packages / reactivity / SRC / effect.ts - line 330

Execute the effect function, that is, the update in distribute update

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // Traverse the set function of the effect
  for (const effect of isArray(dep) ? dep : [...dep]) {
    /** 
      Judge effect here== The reason for activeeffect is that it cannot be the same as the current effect
      For example: count.value + +, if this is an effect, the getter will be triggered, and track collects the currently active effect,
      Then count.value = count.value+1 will trigger the setter and execute the trigger,
      It will fall into an endless loop, so filter the current effect
    */
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      // If the scheduler executes, the calculation attribute has a scheduler
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        // Execute the effect function
        effect.run()
      }
    }
  }
}

Create ref

Source address: packages/reactivity/src/ref.ts

The main purpose here is to deal with ref. first, let's take a look at some related functions, which will be used later

// Judge whether it is ref
export function isRef(r: any): r is Ref {
  return Boolean(r && r.__v_isRef === true)
}
// Create ref
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) { // If it is already ref, it will be returned directly
    return rawValue
  }
  // Call RefImpl to create and return ref
  return new RefImpl(rawValue, shallow)
}
// Create a shallow ref
export function shallowRef(value?: unknown) {
  return createRef(value, true)
}
// Uninstall a ref
export function unref<T>(ref: T | Ref<T>): T {
  return isRef(ref) ? (ref.value as any) : ref
}

RefImpl

Source address: packages / reactivity / SRC / ref.ts - line 87

From the above, we know that the ref object is created through new RefImpl(). The implementation of the RefImpl class is relatively simple. There is no more nonsense here. Please see the comments

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  // There is one under each ref instance__ v_ Read only attribute of isref, identifying it as a Ref
  public readonly __v_isRef = true

  constructor(value: T, public readonly _shallow: boolean) {
    // Judge whether it is shallow comparison. If not, take the old value
    this._rawValue = _shallow ? value : toRaw(value)
    // Judge whether it is shallow comparison. If not, call convert. If it is an object, call reactive()
    this._value = _shallow ? value : convert(value)
  }
  // ref.value takes this value
  get value() {
    // Perform dependency collection
    trackRefValue(this)
    return this._value
  }
  set value(newVal) {
    // If it is a shallow comparison, take the new value instead of the old value
    newVal = this._shallow ? newVal : toRaw(newVal)
    // Compare old and new values
    if (hasChanged(newVal, this._rawValue)) {
      // Value has been changed, re assign
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      // Distribute updates
      triggerRefValue(this, newVal)
    }
  }
}

trackRefValue()

Source address: packages / reactivity / SRC / ref.ts - line 29

The main work here is to do some work on ref dependency collection. It is mainly to determine whether effect has been activated, whether there has been a collection of dependent effect, not to create a dep, to prepare for collection, and then to invoke trackEffects on this article for dependency collection.

export function trackRefValue(ref: RefBase<any>) {
  // If effect is activated, it is collected
  if (isTracking()) {
    ref = toRaw(ref)
    // If the attribute has not collected dependent functions, a dep is created to store dependent effect s
    if (!ref.dep) {
      ref.dep = createDep()
    }
    // development environment 
    if (__DEV__) {
      trackEffects(ref.dep, {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      // Call the trackEffects collection dependency above this article
      trackEffects(ref.dep)
    }
  }
}

triggerRefValue()

Source address: packages / reactivity / SRC / ref.ts - line 47

The source code of the ref distribution update here is relatively simple. It's nothing to say. Just distinguish the development environment, and then execute the trigger effects above to update the corresponding effect function

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  if (ref.dep) {
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(ref.dep)
    }
  }
}

Here, the source code of Vue3's responsive object is basically analyzed

Previous highlights

epilogue

If this article is of little help to you, please give me a praise and support. Thank you

Tags: Javascript Front-end Vue Vue.js source code

Posted on Wed, 20 Oct 2021 02:35:22 -0400 by XtacY