Touch your hand, take you to understand Vue's Computed principle

Keywords: Javascript Attribute Vue

preface

Computed is a very common property configuration in Vue. It can change with the change of dependent properties, which brings us great convenience. So this article will take you to fully understand the internal principle and workflow of computed.

Before that, I hope you can have some understanding of the responsive principle, because computed works based on the responsive principle. If you don't know a lot about responsive principles, read my last article: Touch and take you to understand Vue's responsive principle

computed usage

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

First, function declaration:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // getter of calculated property
    reversedMessage: function () {
      // `this' points to vm instance
      return this.message.split('').reverse().join('')
    }
  }
})

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]
    }
  }
}

Warm tip: data attribute used in computed, hereinafter referred to as "dependency attribute"

Workflow

First, let's understand the general process of computed to see what the core point of computing attributes 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

_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 options and the options passed in
      // The $options here can be understood as the object passed in when 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
    // Initialization data
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

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 */)
  }
  // Compute will be initialized here
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

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 (!(key in vm)) {
      // 4
      defineComputed(vm, key, userDef)
    }
  }
}
  1. Defined on instance_ Computedwatches object, which stores the calculation property Watcher
  2. To get the getter of the calculated property, you need to judge whether it is a function declaration or an object declaration
  3. Create a "calculation property watcher", and the getter is passed in as a parameter. It will be called when the dependency property is updated, and the calculation property will be re valued. You need to pay attention to Watcher's lazy configuration, which is the identity of implementing cache
  4. defineComputed data hijacking of calculation attributes

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
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    // 2
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    // 3
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // 4
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
  1. sharedPropertyDefinition is the initial property description object of calculated property
  2. When calculating a property using function declaration, set the get and set of the property description object
  3. When calculating properties using object declarations, set properties to describe the get and set of the object
  4. Data hijacking is performed on the calculated property, and the sharedPropertyDefinition is passed in as the third parameter

The client side uses createComputedGetter to create get, and the server side uses createGetterInvoker to create get. They are very different. The server-side rendering does not cache the calculated attributes, but directly evaluates them:

function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

But we usually talk more about client-side rendering. Let's take a 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
    }
  }
}

This is the core of the implementation of the calculation property. computedGetter is the get triggered by the data hijacking of the calculation property.

  1. In the initComputed function above, the calculation attribute Watcher is stored in the instance's_ On computedWatchers, take the corresponding "calculation property Watcher" here
  2. watcher.dirty It is the trigger point to realize the calculation property cache, watcher.evaluate Reevaluate calculated properties
  3. Dependent property collection render Watcher
  4. After evaluating the calculated property, the value will be stored in value, and get will return the value of the calculated property

Calculation property cache and update

cache

Let's split the createComputedGetter and analyze their separate workflows. This is the trigger point for the cache:

if (watcher.dirty) {
  watcher.evaluate()
}

Let's look at the implementation of Watcher:

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
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.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()
    this.newDepIds = new Set()
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

Remember to create the "calculation property Watcher" with configured lazy as true. The initial value of dirty is equal to lazy. Therefore, when initializing page rendering and taking value for calculation attribute, it will execute once watcher.evaluate .

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

Assign value to after evaluation this.value , in createComputedGetter above watcher.value It's here to update. Then, set dirty to false. If the dependency property does not change, the next value will not be executed watcher.evaluate It's a direct return watcher.value , which implements the caching mechanism.

to update

When the dependency property is updated, the dep.notify :

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

Then execute watcher.update :

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

Since the lazy of "calculated attribute Watcher" is true, the dirty will be set to true here. When the page rendering takes the value of the calculated attribute, execute watcher.evaluate Reevaluate and the calculated properties update.

Dependency property collection dependency

Collect calculation property Watcher

When initialized, page rendering stacks the render Watcher and mounts it to Dep.target

Calculation property encountered during page rendering, so execute watcher.evaluate Internal call this.get :

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm) // Evaluate calculated properties
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    popTarget()
    this.cleanupDeps()
  }
  return value
}
Dep.target = null
let stack = []  // Stack for storing watcher

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

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

pushTarget turns to "calculation property Watcher" and mounts to Dep.target , the stack is [render Watcher, calculate attribute Watcher]

this.getter Evaluate the calculated property. When obtaining the dependent property, trigger the data hijacking of the dependent property to get and execute dep.depend Collect dependencies ("calculation property Watcher")

Collect render Watcher

this.getter After the evaluation, popTragte, the "calculation attribute Watcher" will be released, Dep.target Set to render Watcher, the Dep.target Is render Watcher

if (Dep.target) {
  watcher.depend()
}

watcher.depend Collection dependency:

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

deps stores attribute dependent deps. This step is to collect dependency (rendering Watcher)

After the above two collections of dependencies, the subs of the dependent properties store two watchers, [calculate attribute Watcher, render Watcher]

Why do dependency properties collect render watchers

When I read the source code for the first time, it's strange that relying on Attribute Collection to "calculate attribute Watcher" is not good? Why collect render Watcher for dependent properties?

The first scenario: dependency attribute and calculation attribute are used in the template at the same time

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

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

The template is useful to the dependency attribute. When the page rendering takes the value of the dependency attribute, the dependency attribute stores the "render watcher", so watcher.depend This step belongs to repeated collection, but the Watcher will be de duplicated internally.

That's why I have doubts. Vue, as an excellent framework, certainly has its own reason to do so. So I came up with another scenario that makes sense watcher.depend The role of.

Second scenario: only calculation attributes are used in the template

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

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

Dependency properties are not used on the template. When rendering a page, the dependency properties will not collect the render Watcher. At this time, there will only be "calculation attribute Watcher" in the dependency attribute. When the dependency attribute is modified, only the update of "calculation attribute Watcher" will be triggered. In the update of the calculated property, only the dirty is set to true, and it is not evaluated immediately, so the calculated property will not be updated.

Therefore, you need to collect render Watcher, and then execute render Watcher after performing calculate attribute Watcher. Page rendering takes value of calculation attribute, and executes watcher.evaluate The evaluation will be recalculated and the page calculation property will be updated.

summary

The principle of computing attribute and the principle of response are the same. The same is the use of data hijacking and dependency collection. The difference is that the computing attribute has cache optimization, and only when the dependency attribute changes will it be re evaluated. In other cases, the cache value is returned directly. The server does not cache computing properties.

The premise of calculating attribute updates requires the cooperation of "render Watcher", so at least two watchers will be stored in the attribute dependent subs.

Posted by loudrake on Fri, 26 Jun 2020 01:17:48 -0700