Many times, it is not clear when to use Vue’s computed properties and when to use Watch to listen on properties. Now let’s take a look at the similarities and differences between the two from a source point of view.

computed

The initialization of computed properties takes place in the initState() function of the Vue instance initialization phase, where there is an initComputed function. The definition of the function in the SRC/core/instance/state in js:

const computedWatcherOptions = {  lazy: true }
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  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]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if(process.env.NODE_ENV ! = ='production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}". `,
        vm
      )
    }

    if(! isSSR) {// create internal watcher for the computed property.
      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.
    if(! (keyin vm)) {
      defineComputed(vm, key, userDef)
    } else if(process.env.NODE_ENV ! = ='production') {
      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

You start by creating an empty object, and then iterate over each key in the computed property, creating a Watcher for each key. This Watcher differs from regular Watcher in that it is lazy Watcher. We’ll expand on the difference between lazy Watcher and regular Watcher later. Then call defineComputed(VM, key, userDef) to determine if the key is not an attribute in the instance VM, otherwise raise the appropriate warning.

Next, focus on the implementation of defineComputed(VM, key, userDef) :

export function defineComputed (target: any, key: string, userDef: Object | Function) {
  constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else{ sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.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)}}Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code

The logic here is simple: Add getters and setters to the key value of the calculated property using Object.defineProperty. The final getter corresponds to the return value of the createdComputedGetter(key). Let’s look at its definition:

function createComputedGetter (key) {
  return function computedGetter () {
    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

CreatedComputedGetter (key) returns a function computedGetter, which is the getter for the computed property.

At this point, the entire initialization process of the calculated properties is complete. We know that the calculated Watcher is a lazy Watcher. What’s the difference between a lazy Watcher and a regular Watcher? To analyze the implementation of lazy Watcher, use an example:

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

The constructor logic is slightly different when initializing the entire lazy Watcher instance:

constructor( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) {/ /...
  this.value = this.lazy
      ? undefined
      : this.get()
}
Copy the code

You can see that lazy Watcher does not immediately evaluate, but instead returns undefined.

Then when our render function calls this.fullname, it starts calculating the property’s getter:

function computedGetter () {
    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

These few lines of code are the core logic. The watcher. dirty attribute is true. Do watcher.evaluate () :

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

Here, the get function for the corresponding attribute is executed by calling this.get(). In our case, execute:

function () {
      return this.firstName + ' ' + this.lastName
}
Copy the code

At this point, the corresponding variables firstName and lastName are fetched, triggering the corresponding reactive procedure. Once you have the latest value, set the this.dirty property to false.

The more critical code is here:

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

The Vue instance has a Watcher that invokes calculated properties. Among the calculated properties is lazy Watcher, which invokes reactive properties. Every Watcher’s get() method has pushTarget(this) and popTarget().

In the above code, the dep. target is the instance Watcher of Vue, and the Watcher variable is the lazy Watcher that evaluates the attribute. By executing the code watcher.depend(), Deps that associate lazy Watcher with evaluated attributes are all associated with dep.target.

In our case, we associate this.firstName and this.lastName with the instance Watcher. In this way:

  • whenthis.firstName,this.lastNameWhen changes occur, the instanceWatcherYou will be notified of the update, and the calculated property will also trigger the GET function to update.
  • whenthis.firstName,this.lastNameWhen there is no change, the instanceWatcherThe call computes the property becauselazy WatcherThe correspondingdirtyProperties forfalse, then it will be returned directly to the cachevalueValue.

The lazy Watcher in the calculated attribute does the following:

  • Get functions (methods) that save computed attributes
  • Save calculation results
  • Control whether cached calculations are valid (via this.dirty)

Watch

The initialization of listening properties, similar to that of evaluating properties, takes place in the initState() function of the Vue instance initialization phase, which has an initWatch function. The definition of the function in the SRC/core/instance/state in js:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
Copy the code

If the handler is an array, iterate through the array and call createWatcher:

function createWatcher (vm: Component, expOrFn: string | Function, handler: any, options? : Object) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
Copy the code

$watch(keyOrFn, handler, options); $watch(keyOrFn, options); It is defined when stateMixin is executed:

Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options? : Object) :Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}return function unwatchFn () {
      watcher.teardown()
    }
  }
Copy the code

That is, the listening property watch will eventually call the $watch method, which will first determine if cb is an object and call createWatcher. This is because the $watch method can be called directly by the user, passing an object as well as a function.

Finally, const watcher = new watcher (VM, expOrFn, CB, options) instantiates a watcher. One thing to note here is that this is a user Watcher because options.user = true. By instantiating Watcher, once our watch’s data changes, it will eventually execute Watcher’s run method, executing the callback function cb.

$watch = vm.$watch = vm.$watch = vm.$watch = vm.

run () {
    if (this.active) {
      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
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
Copy the code

There is a new article about error handling within Vue, which can be found here.

conclusion

In terms of application scenarios, computed attributes are suitable for template rendering, where a value is computed depending on other responsive objects or even computed attributes. The listening attribute is suitable for observing changes in a value to complete a complex piece of business logic.


Vue source code interpretation

(1) : Vue constructor and initialization process

(II) : Data response and implementation

(3) : array responsive processing

(iv) Vue’s asynchronous update queue

(5) The introduction of virtual DOM

(VI) Data update algorithm — Patch algorithm

(7) : realization of componentization mechanism

(8) : computing properties and listening properties

The Optimize stage of the compilation process

Vue

Vue’s error handling mechanism

Parse the working process of Vue in handwritten code

Vue Router handwriting implementation