Study notes and keep adding to them

1. Example (website)

var vm = new Vue({
  data: { a: 1 },
  computed: {
    // Calculates the getter for the property
    / / read only
    aDouble: function () {
      // 'this' points to the VM instance
      return this.a * 2
    },
    // Read and set
    aPlus: {
      get: function () {
        return this.a + 1
      },
      set: function (v) {
        this.a = v - 1
      }
    }
  }
})
vm.aPlus   / / = > 2
vm.aPlus = 3
vm.a       / / = > 2
vm.aDouble / / = > 4
Copy the code

The result of the evaluated property is cached, except for recalculation of dependent responsive property changes. Note that if a dependency (such as a non-reactive property) is outside the instance scope, the calculated property is not updated.

What is a calculated attribute? Why is it cached? Let’s move on

2. Source code parsing (2.5.17beta version, more calculation, less rendering)

Here combined with a scene example, intuitive analysis of the implementation of the calculation of attributes

var vm = new Vue({
  data: {
    firstName: 'Foo'.lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

Copy the code
1. InitComputed ()

First, during vUE initialization, computed computed properties are initialized in initState () and determined to be executed if computed properties are defined

InitComputed () method, as follows

if (opts.computed) initComputed(vm, opts.computed)
Copy the code

Take a look at this initComputed () method, which has four main steps

  • Create an empty object to hold watcher
  • For a computed attribute traversal, get each computed attribute defined
  • Create a Watcher for each calculated property
  • Evaluate each calculated property iterated. If it is not a property on the VM, execute defineComputed (). If it is a property on the VM, determine if the current calculated property name is defined in data and props

Take a look at the code and comments below

const computedWatcherOptions = { computed: true } // Instantiate the Watcher configuration item

function initComputed (vm: Component, computed: Object) {
  Create empty object Watchers, vm._computedWatchers
  const watchers = vm._computedWatchers = Object.create(null)
  // Determine if it is server side rendering (SSR), here we analyze the browser environment
  const isSSR = isServerRendering()
  // Step 2. For computed attribute traversal
  for (const key in computed) {
    const userDef = computed[key] // Get the value of each calculated attribute defined
    / / to get the calculation of the value of the above, whether the function, if the function, direct assignment to getter, people would not calculate attribute get assigned to getter (see the website calculate attribute definitions, -- -- -- -- > sample)
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // Warning if the calculated property does not define a getter
    if(process.env.NODE_ENV ! = ='production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}". `,
        vm
      )
    }

    if(! isSSR) {// Step 3. Create a Watcher for each getter (this can be called computed Watcher, and instantiated computed Watcher is analyzed below)
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // At instantiation here. The calculated properties defined by the component are already defined on the component prototype
    
    // Define the key, if it is not a vm attribute, and call defineComputed () for reactive processing
    if(! (keyin vm)) {
      defineComputed(vm, key, userDef)
    } else if(process.env.NODE_ENV ! = ='production') {
      // Warning if key is already defined in data, or props
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
Copy the code

Let’s take a look at some of the processing done in the four steps above

2. DefineComputed ()

The purpose of this method is to add getters and setters to each calculated property using Object.defineProperty () to make it responsive

// Define an Object as an argument to Object.defineProperty ()
const sharedPropertyDefinition = {
  enumerable: true.configurable: true.get: noop, / / empty function
  set: noop
}
export function defineComputed (
  target: any,
  key: string, // The name of the calculated property defined
  userDef: Object | Function // Compute the processing done to the attribute
) {
  // Add get and set handlers to the sharedPropertyDefinition object
  constshouldCache = ! isServerRendering()// Non-server rendering
  if (typeof userDef === 'function') { / / function
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else { / / objectsharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  } 
  if(process.env.NODE_ENV ! = ='production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`.this)}}// Finally, the calculated property is processed by calling Object.defineProperty
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code

In normal development scenarios, there are few setters for computing properties. We will mainly analyze getters. The above definition of get function will enter createComputedGetter () in our common browser environment. So let’s see what this function does, and keep going, okay

3. CreateComputedGetter ()

The final getter corresponds to the return value of createComputedGetter (), as follows

function createComputedGetter (key) {
  return function computedGetter () {
    // Get this._computedWatchers. This is the object defined on the VM above in initComputed, accessible through this, which holds the watcher for each calculated attribute. That's the Watcher for every calculated property that you get
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // Call watcher.depend() and return watcher.evaluate(). Look at the following four
      watcher.depend()
      return watcher.evaluate()
    }
  }
}
Copy the code

For the returned function, call the Depend () and evaluate () methods, which are defined in the Watcher class as follows

4. New Watcher () for calculating attributes

So Watcher is a class that’s defined, so I’m just going to take the code that defines Watcher, it’s a little bit too much, and you can look at it, and the ones that have to do with calculating properties, I’ll take them out later

/* @flow */

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError
} from '.. /util/index'

import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import type { SimpleSet } from '.. /util/index'

let uid = 0

/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  computed: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  dep: Dep;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !! options.deepthis.user = !! options.userthis.computed = !! options.computedthis.sync = !! options.syncthis.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {} process.env.NODE_ENV ! = ='production' && warn(
          `Failed watching path: "${expOrFn}"` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }

  /** * Evaluate the getter, and re-collect dependencies. */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /** * Add a dependency to this directive. */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)}}}/** * Clean up for dependency collection. */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)}}let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /** * Subscriber interface. * Will be called when a dependency changes. */
  update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() = > {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */
  run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

  getAndInvoke (cb: Function) {
    const value = this.get()
    if( value ! = =this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }

  /** * Evaluate and return the value of the watcher. * This only gets called for computed property watchers. */
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

  /** * Depend on this watcher. Only for computed property watchers. */
  depend () {
    if (this.dep && Dep.target) {
      this.dep.depend()
    }
  }

  /** * Remove self from all dependencies' subscriber list. */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)}let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)}this.active = false}}}Copy the code

When you execute initComputed () above, in what we call step 3, walk through the instantiation to create a watcher (computed Watcher) for the calculated properties

  watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions  // const computedWatcherOptions = { computed: true }
      )
Copy the code

Just to simplify new Watcher, let’s see

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object, isRenderWatcher? : boolean) {
  
  // ...
  
  if (this.computed) { // 判断this.computed
    this.value = undefined
    this.dep = new Dep()
  } else {
    this.value = this.get() // Call get() to evaluate}}Copy the code

Here you can see that the Watcher calculating the property does not evaluate immediately, but instead judges this.computed while instantiating a DEP instance.

In the process of rendering data to the page, if the vue accesses the defined calculated property, it fires the getter for the calculated property (the getter is the return value of the createComputedGetter () function) and gets the watcher for the calculated property. It then executes the Depend method in Watcher and returns the evaluate value

  if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }
Copy the code

Depend method

/** * Depend on this watcher. Only for computed property watchers. */
depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}
Copy the code

Note that dep. target is rendering watcher, so this.dep.depend() corresponds to rendering Watcher subscribed to this computed Watcher change.

Then perform the watcher.evaluate () evaluation

The evaluate () function first evaluates this.dirty. If true, it calls get () to evaluate it. Get () is also defined in the Watcher class

Value = this.getter.call(vm, vm), which calls the getter defined in the calculation property, evaluates it, sets this.dirty to false, and returns the desired value

/** * Evaluate and return the value of the watcher. * This only gets called for computed property watchers. */
evaluate () {
  if (this.dirty) {
    this.value = this.get()
    this.dirty = false
  }
  return this.value
}
Copy the code

The important thing to note here is that since this.firstName and this.lastName are both responsive objects, their getters are fired and, according to our previous analysis, they will add their own DEP to the watcher currently being evaluated. In this case, DEP. Target is this computed watcher.

5. Modify the data that computing attributes depend on

Once we modify the data on which the calculated property depends, the setter process is triggered to notify all watcher updates subscribed to the change (dep.notify ()).

 notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
Copy the code

As above, iterate through the loop and execute watcher’s update () method

 update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() = > {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}Copy the code

If this.dep.subs.length === 0, no one subscribed to the change of the calculated watcher. Set this.dirty to true. So that the evaluation will only be reevaluated the next time the property is accessed.

In our example scenario, rendering watcher subscribed to this computed Watcher change, then it would perform:

 this.getAndInvoke(() = > {
      this.dep.notify()
   })
Copy the code
getAndInvoke (cb: Function) {
    const value = this.get() // will recalculate
    // The old and new values are compared, and if they change, the incoming callback is executed
    if( value ! = =this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }
Copy the code

The getAndInvoke function recalculates and compares the old and new values. If it changes, the callback function is executed, in this case this.dep.notify(), which in our case triggered the rendering watcher to re-render

Here to compare the calculation on properties of the old and new values, this ensures the, when calculating the value of attribute dependence of change, he will be recalculated, but not necessarily to render, only calculated the new value relative to the old value changed, will trigger the rendering watcher to render, this is essentially an optimization of the vue, much calculation, less updates

3. Source code parsing (2.6.13 version, more rendering, less calculation)

In the Update method of Watch, there is no getAndInvoke method. Instead of comparing the old and new values calculated by the calculated property, updateComponent is triggered to update the render regardless of whether they are equal or not

Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this); }};Copy the code

Look at the previous version (2.5.17 Beta)

Watcher.prototype.update = function update () {
    var this$1 = this;

  /* istanbul ignore else */
  if (this.computed) {
    // A computed property watcher has two modes: lazy and activated.
    // It initializes as lazy by default, and only becomes activated when
    // it is depended on by at least one subscriber, which is typically
    // another computed property or a component's render function.
    if (this.dep.subs.length === 0) {
      // In lazy mode, we don't want to perform computations until necessary,
      // so we simply mark the watcher as dirty. The actual computation is
      // performed just-in-time in this.evaluate() when the computed property
      // is accessed.
      this.dirty = true;
    } else {
      // In activated mode, we want to proactively perform the computation
      // but only notify our subscribers when the value has indeed changed.
      this.getAndInvoke(function () {
        this$1.dep.notify(); }); }}else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this); }};Copy the code

Take a look at the getAndInvoke () method

Watcher.prototype.getAndInvoke = function getAndInvoke (cb) {
  var value = this.get();
  // If the calculated new value is different from the old value, the execution will continue. If the calculated new value is different, the execution will not continue
  if( value ! = =this.value ||   
    // Deep watchers and watchers on Object/Arrays should fire even
    // when the value is the same, because the value may
    // have mutated.
    isObject(value) ||
    this.deep
  ) {
    // set new value
    var oldValue = this.value;
    this.value = value;
    this.dirty = false;
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue);
      } catch (e) {
        handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\" ")); }}else {
      cb.call(this.vm, value, oldValue); }}};Copy the code