preface

Computed is a very common attribute configuration in Vue, and it is very convenient for us to change with dependent attributes. So this article gives you a full understanding of computed internals and how it works.

Before I do that, I hope you have some understanding of the responsive principle, because computed works on the responsive principle. If you are not familiar with the responsivity principle, you can read my previous article: Hand Touch guide to Understanding the Responsivity principle of Vue

The computed usage

To understand a principle, the most basic thing is to know how to use it, which is helpful for later understanding.

First, function declarations:

var vm = new Vue({
  el: '#example'.data: {
    message: 'Hello'
  },
  computed: {
    // Calculates the getter for the property
    reversedMessage: function () {
      // 'this' points to the VM instance
      return this.message.split(' ').reverse().join(' ')}}})Copy the code

Second, object declaration:

computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]}}}Copy the code

Note: Data attributes used in computed data are collectively referred to as “dependent attributes”

The working process

Let’s take a look at how computations work in general, and see what the core point of a computed attribute is.

Entry file:

/ / source location: / SRC/core/instance/index, js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '.. /util/index'

function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
Copy the code

_init:

/ / source location: / SRC/core/instance/init. Js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options? : Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // mergeOptions merges the mixin option with the options passed in by new Vue
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // Initialize the data
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
Copy the code

initState:

/ / source location: / SRC/core/instance/state. Js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)}// Computed is initialized here
  if (opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

initComputed:

/ / source location: / SRC/core/instance/state. Js
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  / / 1
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
    
  for (const key in computed) {
    const userDef = computed[key]
    / / 2
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if(! isSSR) {// create internal watcher for the computed property.
      / / 3
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true})}// component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if(! (keyin vm)) {
      / / 4
      defineComputed(vm, key, userDef)
    }
  }
}
Copy the code
  1. Instantiated_computedWatchersObject for storing the calculated propertiesWatcher
  2. Of the computed propertygetterYou need to decide whether it is a function declaration or an object declaration
  3. Create compute propertiesWatcher“,getterPassed as a parameter, it is called when a dependent property is updated and revalues the evaluated property. Need to pay attention toWatcherlazyConfiguration, which is the identity that implements caching
  4. defineComputedData hijacking of computed properties

defineComputed:

/ / source location: / SRC/core/instance/state. Js
const noop = function() {}
/ / 1
const sharedPropertyDefinition = {
  enumerable: true.configurable: true.get: noop,
  set: noop
}

export function defineComputed (target: any, key: string, userDef: Object | Function) {
  // Determine whether to render for the server
  constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
    / / 2
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    / / 3sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  / / 4
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code
  1. sharedPropertyDefinitionIs the property description object that computes the property initially
  2. When you use a function declaration, set the properties describing the objectgetset
  3. When you use an object declaration, set the properties that describe the objectgetset
  4. Data hijacking of calculated properties,sharedPropertyDefinitionPassed in as the third parameter

The client render creates the GET using createComputedGetter, and the server render creates the GET using createGetterInvoker. There is a big difference between the two. Server-side rendering does not cache computed attributes, but evaluates them directly:

function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this.this)}}Copy the code

But we usually talk more about client-side rendering, so let’s look at the implementation of createComputedGetter.

createComputedGetter:

/ / source location: / SRC/core/instance/state. Js
function createComputedGetter (key) {
  return function computedGetter () {
    / / 1
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      / / 2
      if (watcher.dirty) {
        watcher.evaluate()
      }
      / / 3
      if (Dep.target) {
        watcher.depend()
      }
      / / 4
      return watcher.value
    }
  }
}
Copy the code

This is the core of the implementation of a calculated property, and a computedGetter is the get triggered by a calculated property when data hijacking is performed.

  1. In the aboveinitComputedFunction, “evaluates attributesWatcher“Is stored in the instance_computedWatchersHere extract the corresponding “calculated attribute”Watcher
  2. watcher.dirtyIs the trigger point for implementing the compute attribute cache,watcher.evaluateReevaluate the calculated property
  3. Rely on property collection “renderWatcher
  4. The evaluated property stores the value invalue,getReturns the value of the calculated property

Calculate the property cache and update

The cache

Let’s split the createComputedGetter and examine their individual workflows. This is the cache trigger:

if (watcher.dirty) {
  watcher.evaluate()
}
Copy the code

Let’s look at the Watcher implementation:

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  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
    // options
    if (options) {
      this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    // The initial value of dirty is equal to lazy
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set(a)this.newDepIds = new Set(a)this.value = this.lazy
      ? undefined
      : this.get()
  }
}
Copy the code

Remember to create “compute property Watcher” with lazy set to true. The initial value of dirty is the same as lazy. So when you initialize the page rendering and evaluate the evaluated property, you do watcher.evaluate once.

evaluate() {
  this.value = this.get()
  this.dirty = false
}
Copy the code

Value is evaluated and assigned to this.value, where the watcher.value in the createComputedGetter above is updated. Evaluate is not executed the next time it is evaluated. Instead, watcher.value is returned.

update

When a dependent property is updated, dep.notify is called:

notify() {
  this.subs.forEach(watcher= > watcher.update())
}
Copy the code

Then execute watcher.update:

update() {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)}}Copy the code

Since the “compute attribute Watcher” has lazy true, dirty will be set to true. When the page rendering evaluates the calculated property and the trigger point condition is met, perform watcher.evaluate re-evaluation and the calculated property is updated.

Dependency properties collect dependencies

Collect calculated properties Watcher

When initialized, the page rendering pushes the “Render Watcher” onto the stack and mounts it to dep.target

Evaluate the value of an evaluated property encountered during page rendering, so execute the watcher.evaluate logic and then call this.get:

get () {
  / / 1
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    / / 2
    value = this.getter.call(vm, vm) // Evaluate attributes
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
      throw e
    }
  } finally {
    popTarget()
    this.cleanupDeps()
  }
  return value
}
Copy the code
Dep.target = null
let stack = []  // Store the watcher stack

export function pushTarget(watcher) {
  stack.push(watcher)
  Dep.target = watcher
} 

export function popTarget(){
  stack.pop()
  Dep.target = stack[stack.length - 1]}Copy the code

PushTarget = ‘render Watcher, calculate Watcher’; pushTarget = ‘render Watcher, calculate Watcher’;

Enclosing the getter to calculate attribute evaluation in rely on attributes, trigger based on attribute data hijacked a get, execute dep. Depend dependent on collection (” calculate attribute Watcher “)

Collect render Watcher

After this. Getter is evaluated, popTragte “Calculate property Watcher” is removed from stack, dep. target is set to “Render Watcher”, dep. target is “Render Watcher”.

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

Watcher. Depend Collect dependencies:

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

The DEPS stored in dePS are attribute-dependent DEPs. This step is the dependency collection dependency (” Render Watcher “)

After the above two dependency collections, the subs of the dependent property store two Watcher, [calculate the property Watcher, render the Watcher]

Why do dependent properties collect render Watcher

When I read the source code for the first time, I was surprised to find that the “calculated property Watcher” was just fine. Why collect “render Watcher” depending on attributes?

The first scenario involves using both dependent and computed properties in a template

<template>
  <div>{{msg}} {{msg1}}</div>
</template>

export default {
  data() {return {
      msg: 'hello'
    }
  },
  computed:{
    msg1() {return this.msg + ' world'}}}Copy the code

The template is useful for dependency properties, which store “render Watcher” when the page renders the value of dependency properties, so the Watcher. Depend step is collected repeatedly, but the Watcher is de-duplicated internally.

This is why I have a question. Vue is a great framework and has a point. So I thought of another scenario that would explain watcher. Depend.

Second scenario: only computed attributes are used in the template

<template>
  <div>{{msg1}}</div>
</template>

export default {
  data() {return {
      msg: 'hello'
    }
  },
  computed:{
    msg1() {return this.msg + ' world'}}}Copy the code

Dependency properties are not used on the template, so dependency properties do not collect “render Watcher” when the page is rendered. In this case, only “calculated Watcher” will be displayed in the dependency property. When the dependency property is modified, only the update of “calculated Watcher” will be triggered. An update to the calculated property simply sets dirty to true and does not evaluate immediately, so the calculated property will not be updated.

So you need to collect “render Watcher” and execute “Render Watcher” after executing “Calculate property Watcher”. The page renders the evaluated property and executes Watcher. Evaluate to recalculate the evaluated property and update the page evaluated property.

conclusion

The principle of calculating attributes and the principle of responsivity are almost the same, the same is the use of data hijacking and dependency collection, but the difference is that the calculation of attributes has cache optimization, only when the dependent attributes change will be re-evaluated, other cases are directly returned cache value. The server does not calculate the property cache.

A “render Watcher” is required to calculate a property update, so at least two Watchers are stored in subs that depend on the property.