When it comes to computations, one of the things most commonly mentioned is that computed attributes are lazily evaluated, and that the values of computed attributes are cached and recalculated only when the dependent responsive data is updated.

Do you really understand why? Have you ever thought:

  1. How is the page notified to rerender after a calculated property change?
  2. How does the reactive data associated with the calculated properties fit togetherThe Watcher instance that evaluates the propertyAdd your ownObserver queue?

computed

During component initialization, analogous to props, data, and Watch, computed properties in the component are initialized inside initState.

Aside from the SSR scenario, take the following usage as an example:

{
  data() {
    return {
      a: 1.b: 2}},computed: {
    c() {
      return this.a + this.b
    }
  },
}
Copy the code

Initialize the

// Initialize computed
function initComputed(vm: Component, computed: Object) {
  // The created Watcher instance is stored in the component instance's '_computedWatchers' object
  const watchers = (vm._computedWatchers = Object.create(null))

  // Iterate over the definitions of all computed attributes
  for (const key in computed) {
    const userDef = computed[key]

    const getter = typeof userDef === 'function' ? userDef : userDef.get

    / / = = = = = = = = = would create a watcher instance for each computed key = = = = = = = = = / /
    / / = = = = = = computed is the nature of a ` lazy: true ` Watcher instance = = = = = = = = / /
    watchers[key] = new Watcher(vm, getter || noop, noop, { lazy: true })
    // ...}}Copy the code

Inertia is evaluated

Because of {lazy: true}, the Watcher that evaluates the property does not immediately call the getter to evaluate it during initialization, which is the first manifestation of lazy evaluation.

Function c() {return this.a + this.b} function c() {return this.a + this.b}

// The code is truncated
class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    vm._watchers.push(this)
    // ...
    this.dirty = this.lazy = !! options.lazy// ...
    this.getter = expOrFn
    // ...
    // Calculate the watcher of the attribute, whose lazy attribute is true
    this.value = this.lazy ? undefined : this.get()
  }

  get() {
    pushTarget(this)

    const vm = this.vm
    let value = this.getter.call(vm, vm)

    popTarget()
    return value
  }

  // Call this method after the dependent data changes
  update() {
    if (this.lazy) {
      // Dirty will be set to true
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}evaluate() {
    this.value = this.get()
    // Dirty will be set to false
    this.dirty = false}}Copy the code

dirty

In the Watcher class, lazy is assigned and never changes. Dirty is initialized with the value of lazy, but it can change when:

  • inevaluateMethod, after the getter is evaluated, sets the value to false
  • inupdateMethod, if lazy is true, the value is set to true

Here’s what dirty does:

In addition to being lazy, computed values can be cached, and dirty is used to indicate whether the current cached value is still valid.

  • If it isfalse, indicating that the cache is available and no call is requiredgetterrecalculate
  • If it istrue“Indicates that data is cached'dirty', need to recalculate

At this point, it should be clear why dirty is set to true after a dependency update and false after a getter is called to reevaluate

Take a closer look at exactly when evaluate and update are called

evaluation

For computed attributes, in addition to function, you can set getters and setters separately as objects:

{
  computed: {
    c: {
      // getter
      get: function () {
        return this.a + this.b
      },
      // setter
      set: function (newValue) {
        var values = newValue.split(' ')
        this.a = values[0]
        this.b = values[values.length - 1]}}}}Copy the code

At the end of the initialization process, getters and setters for computed properties are configured based on whether the key currently computed exists on the current component instance.

function initComputed() {
  // ...

  // If key does not exist on the current instance
  if(! (keyin vm)) {
    defineComputed(vm, key, userDef)
  }
}
Copy the code

defineComputed

// The code is truncated
function defineComputed(target, key, userDef) {
  // Computed attribute values are a function
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } else {
    // is an object
    sharedPropertyDefinition.get = userDef.get
      ? createComputedGetter(key)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // ...
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter(key) {
  // This is the getter for the calculated property, which is fired when the value of the calculated property is accessed
  return function computedGetter() {
    // Retrieve the corresponding observer
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        / / implement the getter
        watcher.evaluate()
      }
      // ...
      return watcher.value
    }
  }
}
Copy the code

The focus is on the real getter for the calculated property returned in the createComputedGetter method. Again, assuming that the value of the computed property C is used in template interpolation, the computedGetter method here is called

Initial evaluation

  • computedGetterMethod is first extracted from the component instance_computedWatchersProperty gets the property incwatcherInstance (This is a one-to-one relationship). If not, no value is required
  • If it exists, it will be based onwatcher.dirtyThe value of determines whether the evaluation operation is performed. This value is true for the first rendering, so it will be executed hereevaluateMethod to perform an evaluation
    • The value is cached on the Value property of the Watcher instance, and thethis.dirty = false
  • And it returns that value

In this case, due todirtyIs false, so even thoughcIt gets called multiple times, it doesn’t get evaluated multiple times, it gets returnedwatcher.valueCached results

An important step here is the dependency collection process for a and B on which C depends for the Watcher instance that evaluates attribute C. Simply put, this is how attributes A and B add watcher instances that evaluate attributes C to their observer queues so that they can notify C to recalculate the evaluation when they update.

The process is a little more complicated, step by step:

Depend on the collection

  1. The first rendering mentioned above is executedevaluateMethod evaluates c, which means thatThe get method for the Watcher instance that evaluates property C is executed. So herepushTarget(this)This is the Watcher instance that evaluates the property c. At this timeDep.target = this
get() {
  pushTarget(this)

  const vm = this.vm
  let value = this.getter.call(vm, vm)

  popTarget()
  return value
}
Copy the code
  1. this.getterExecution, that isfunction c() { return this.a + this.b }This method is executed. So hereThat triggers the getters for properties A and B. Take A as an example:
/ / a getter
{
  get: function reactiveGetter() {
    const value = getter ? getter.call(obj) : val
    // Rely on collection
    if (Dep.target) {
      dep.depend()
      // ...
    }
    return value
  }
}
Copy the code

Since dep.target is present, the dependency collection process for attribute A is called dep.depend(), which adds the Watcher instance for attribute C to the Watcher queue for attribute A, and for attribute B.

Let’s look at how the calculated property c is recalculated when the dependent property A or B is updated.

To evaluate

Suppose the following scenario: the value of A is reassigned to 20

this.a = 20
Copy the code

Since the value of a is updated, the update method for all watcher instances that observed that value will be called, so the update method for the watcher instance that evaluated c will be called:

  • Because of the calculation of propertieslazy === true, so the dirty attribute is set to true,And again, you’re not evaluating c
// Call this method after the dependent data changes
update() {
  if (this.lazy) {
    // Dirty will be set to true
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)}}Copy the code
  • When c is accessed again in the application, the getter for C is executed, and since the dirty property is true, c is reevaluated
function computedGetter() {
  // Retrieve the corresponding observer
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    if (watcher.dirty) {
      // Execute the getter to evaluate
      watcher.evaluate()
    }
    // ...
    return watcher.value
  }
}
Copy the code

Here are some answers to the questions I raised at the beginning of this article about lazy evaluation, values being cached, and how watcher instances that calculate attributes are added to their data-dependent observer queues. One final question remains: how do I tell the rendering Watcher to re-render the page when the value of the calculated property is updated?

Notify render Watcher

This part is more around, and it is recommended that students who are not familiar with the source code of part of the responsive principle can look at the source code a little bit.

As mentioned above, the evaluation of property C triggers the getters for properties A and B,

/ / a getter
{
  get: function reactiveGetter() {
    const value = getter ? getter.call(obj) : val
    // Rely on collection
    if (Dep.target) {
      dep.depend()
      // ...
    }
    return value
  }
}
Copy the code

Using a as an example, what does dep.depend() do

dep.depend

class Dep {
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)}}}Copy the code

Dep.target refers to the watcher instance that evaluates property C, so

class Watcher {
  // ...
  addDep(dep: Dep) {
    // ...
    this.newDeps.push(dep)
    // ...}}Copy the code

After addDep, the newDeps property of the Watcher instance that evaluates property C has both a and B dependencies.

If the Dep. Target is available, the depend method will be called. If the Dep. Target is available, the depend method will be called.

function computedGetter() {
  // Retrieve the corresponding observer
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    if (watcher.dirty) {
      watcher.evaluate()
    }
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value
  }
}
Copy the code

Who does the dep. target point to? Or watcher instance that evaluates property C?

Obviously not, because when watcher.evaluate() is done, the get method for evaluating the watcher instance of c is done, which means that popTarget has removed the watcher instance of c from the stack.

get() {
  pushTarget(this)
  let value
  try {
    const vm = this.vm
    value = this.getter.call(vm, vm)
  } catch(e) {
    // ...
  } finally {
    popTarget()
    this.cleanupDeps()
  }
  return value
}
Copy the code

watcher.cleanupDeps

Also, in the finally block, cleanupDeps assigns the deps property of the Watcher instance of the calculated property C to newDeps, meaning that dePS also holds two DEP instances of A and B.

class Watcher {
  cleanupDeps() {
    // ...
    this.deps = this.newDeps
    // ...}}Copy the code

When the watcher instance that evaluates property C is popped up, who else is on the stack? That’s the component rendering Watcher. As to why, you can go to understand the component mount process, here is not repeated.

If (dep.target) {watcher. Depend ()} what does the depend method on the watcher instance of c do

watcher.depend

depend() {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}
Copy the code

Since the this.cleanupdeps () method puts the DEP instances of attribute A and b into the DEP queue of the Watcher instance that evaluates attribute C, the queue is traversed and their depend methods are called in turn

depend () {
  if (Dep.target) {
    Dep.target.addDep(this)}}Copy the code

Remember that dep.target already points to the rendering watcher of the current component, so dep.target.adddep (this) results in:

  • Put the render Watcher in a’s Watcher queue
  • Put the render Watcher in b’s Watcher queue

This way, if either A or B changes, the page rendering Watcher will eventually be notified to re-render the page. This answers the last question at the beginning of this article, how to notify the page of re-rendering after a property change is computed. And the order in which the page rendering Watcher and the calculated property C are pushed ensures that when the page is rerendered, the latest value of the calculated property C is always returned.

conclusion

  • Different from the attributes in ordinary data, there is no corresponding Dep instance and no corresponding dependency collection process for calculated attributes.
  • The essence of a calculated property is a lazy evaluation of the watcher instance. The data it depends on adds the watcher instance to its observer queue so that it can tell its Watcher instance to reevaluate when its dependent data changes
  • After the calculation property is re-evaluated, the page is also re-rendered by the render Watcher in the observer queue of the data it depends on, which itself is independent of the render Watcher