SoFunction
Updated on 2025-04-05

Vue3 computed attributes and asynchronous computed attribute methods

1. Brief introduction

Whether it is computational attributes or asynchronous computational attributes, they are implemented based on the overall responsive principle of Vue3. The core is still the ReacetEffect class. If you don't know the principle of the response, it is recommended to look at it firstResponsive Principlechapter.

The difference between computed attributes and regular dynamic responses is that it will not actively execute the callback method associated with ReacteEffect, but instead uses a mark to indicate whether the current value has changed. If there is any change, the callback method is called again. If there is no change, the value calculated last time is directly obtained.

2. Calculate attribute core source code

export type ComputedGetter<T> = (...args: any[]) => T
export type ComputedSetter<T> = (v: T) => void
export interface WritableComputedOptions<T> {
  get: ComputedGetter<T>
  set: ComputedSetter<T>
}
class ComputedRefImpl<T> {
  public dep?: Dep = undefined
  private _value!: T
  private _dirty = true
  public readonly effect: ReactiveEffect<T>
  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    //Storage a ReactiveEffect object internally     = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        //The next time the flag is read, the value will be recalculated        this._dirty = true
        //Trigger dependency update        triggerRefValue(this)
      }
    })
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
  get value() {
    // the computed ref may get wrapped by other proxies . readonly() #3376
    const self = toRaw(this)
    //Collect dependencies    trackRefValue(self)
    //Whether to recalculate the mark    if (self._dirty) {
      //Recalculate      self._dirty = false
      self._value = ()!
    }
    //Sign up the calculated value    return self._value
  }
  set value(newValue: T) {
    this._setter(newValue)
  }
}
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          ('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = 
    setter = 
  }
  //Just pay attention to this  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter)
  if (__DEV__ && debugOptions) {
     = 
     = 
  }
  return cRef as any
}

The generation of a computed attribute object is generated through the computed method. This method is actually to receive a get method and a set method and generate a ComputedRefImpl type object. The implementation of the ComputedRefImpl class is very simple, generating a ReactiveEffect type object and implementing a read and write method of a value attribute.

Collect dependencies while reading and determine whether to recalculate them. The special thing is that this ReactiveEffect type object receives the second parameter.

Let's take a closer look at the code that triggers dependency updates, as follows:

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || ) {
      if (__DEV__ && ) {
        (extend({ effect }, debuggerEventExtraInfo))
      }
      if () {
        ()
      } else {
        ()
      }
    }
  }
}

The logic inside is very simple. It traverses the ReactiveEffect type object inside the dependency. If there is a scheduler method, call this method. We are looking at the definition of the ReactiveEffect class, the code is as follows:

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  // can be attached after creation
  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
  ) {
    recordEffectScope(this, scope)
  }
}

At this time, you can find that the second parameter received by the effect object in the ComputedRefImpl class is the scheduler method. Therefore, when there is a dependent data change, this method will be executed, which is easy to understand.

It means changing the mark, which means that the next time you read this calculated property, you need to recalculate it and cannot use the previous cached value.

Then trigger the dependency update, without doubt, because this computed attribute itself is also responsive, and the changes need to be notified of the corresponding dependency update. As for this judgment, it is 100% true, because triggering a dependency requires adding a dependency first, and adding a dependency when reading a value value will set the flag to false.

3. Asynchronous computing attribute core source code

const tick = ()
const queue: any[] = []
let queued = false
const scheduler = (fn: any) =&gt; {
  (fn)
  if (!queued) {
    queued = true
    (flush)
  }
}
const flush = () =&gt; {
  for (let i = 0; i &lt; ; i++) {
    queue[i]()
  }
   = 0
  queued = false
}
class DeferredComputedRefImpl&lt;T&gt; {
  public dep?: Dep = undefined
  private _value!: T
  private _dirty = true
  public readonly effect: ReactiveEffect&lt;T&gt;
  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY] = true
  constructor(getter: ComputedGetter&lt;T&gt;) {
    let compareTarget: any
    let hasCompareTarget = false
    let scheduled = false
     = new ReactiveEffect(getter, (computedTrigger?: boolean) =&gt; {
      if () {
        if (computedTrigger) {
          compareTarget = this._value
          hasCompareTarget = true
        } else if (!scheduled) {
          const valueToCompare = hasCompareTarget ? compareTarget : this._value
          scheduled = true
          hasCompareTarget = false
          //Join the execution queue          scheduler(() =&gt; {
            if ( &amp;&amp; this._get() !== valueToCompare) {
              triggerRefValue(this)
            }
            scheduled = false
          })
        }
        // chained upstream computeds are notified synchronously to ensure
        // value invalidation in case of sync access; normal effects are
        // deferred to be triggered in scheduler.
        for (const e of ) {
          if () {
            !(true /* computedTrigger */)
          }
        }
      }
      // Ensure that the asynchronous method is recalculated when obtaining the value.      this._dirty = true
    })
     = true
  }
  private _get() {
    if (this._dirty) {
      this._dirty = false
      return (this._value = ()!)
    }
    return this._value
  }
  get value() {
    trackRefValue(this)
    // the computed ref may get wrapped by other proxies . readonly() #3376
    return toRaw(this)._get()
  }
}
export function deferredComputed&lt;T&gt;(getter: () =&gt; T): ComputedRef&lt;T&gt; {
  return new DeferredComputedRefImpl(getter) as any
}

The structure of asynchronous computed attributes and computed attributes is almost the same. The most important difference is the difference in the second parameter of the ReactiveEffect type object.

This method is called when a dependency data is changed. We don’t care about the first if judgment first, and look directly at the content in else. Simply put, it is to put a method into the asynchronous execution queue and then execute asynchronously. Because when the dependency data is changed, the _dirty attribute is set to true, this two-asynchronous execution method will calculate the latest value and trigger the dependency update.

Let’s look at the content in if now, this branch is entered through the following code.

for (const e of ) {
  if () {
    !(true /* computedTrigger */)
  }
}

The design principle here is actually because when the asynchronous calculation attribute is obtained synchronously, the latest value will be obtained. When the asynchronous method is executed, since the data has been retrieved once, in order to ensure that this._get() !== valueToCompare determines that the value is true, valueToCompare must be equal to the previous value recalculating.

This can be explained by the following example:

    const src = ref(0)
    const c1 = deferredComputed(() =&gt; {
      return  % 2
    })
    const c2 = deferredComputed(() =&gt; {
      return  + 1
    })
    effect(() =&gt; {
      
    })
     = 1
    //Synchronous printing, output 2    ();

In the above process, when the value is assigned = 1, c1 executes a callback. Since c2 depends on the value of c1, c2 will also execute a callback. The callbacks here refer to the scheduler method. The _dirty attribute will be set to true, so when synchronously printed values, c2 will be recalculated. At this time, c1's _dirty attribute is also set to true, so c1's value will also be recalculated, that is, c2 printed synchronously will get the latest value.

But when you need to pay attention, the method in the asynchronous queue has not been executed yet. After the synchronization code is executed, the methods in the asynchronous queue are executed, but when the following code is executed:

if ( && this._get() !== valueToCompare) {
    triggerRefValue(this)
}

Because the printed values ​​are synchronized, the _get() method will take the value from the cache. If valueToCompare is not equal to the value before calculation, but is directly equal to this._value, it is judged to be false and the following dependency update method will not be triggered.

The core idea of ​​asynchronous computing attributes is actually just to put the logic that depends on updates into the asynchronous queue and execute it in an asynchronous form. The main logic and computation attributes are almost the same, and are only slightly different in details.

The above is personal experience. I hope you can give you a reference and I hope you can support me more.