Series of articles:

  • Vue source code interpretation (Design)
  • Vue source code interpretation (Rollup)
  • Vue source code interpretation (entry to constructor overall flow)
  • Vue source code interpretation (Responsive principle introduction and Prop)
  • Vue source code Interpretation (Methods, Data)

The computed processing

The logic that deals with computed occurs in initState, and the logic that deals with computed is analyzed in detail.

export function initState (vm: Component) {
  // omit the code
  const opts = vm.$options
  if (opts.computed) initComputed(vm, opts.computed)
}
Copy the code

You know that computed attributes depend on other responsive variables, so there are two steps to analyze computed: computed initialization and computed update.

The computed initialization

The initComputed() method is called if computed is passed in the initState() method. InitComputed () method defined in SRC/core/instance/state. The js file, the code is as follows:

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

Code analysis:

  • ininitComputed()Method first defines a_computedWatchersCaches all computed attributes of the current instancewatcher.
  • And I’m going to go through all of themcomputedAnd then for each onecomputedType judgment, if yesfunctionType, then used directly, or is if it is an objectget/setThe form, we take it directlyget. If you finally get thecomputedthegetterfornull, an error message is displayed in the development environment.
// Two types of computed attributes
export default {
  props: ['index'],
  data () {
    return {
      firstName: 'first'.lastName: 'last'}},computed: {
    fullName () {
      return this.firstName + this.lastName
    },
    active: {
      get: function () {
        return this.index
      },
      set: function (newVal) {
        this.$emit('update:index', newVal)
      } 
    }
  }
}
Copy the code

Using the above code as an example, the getters obtained by the two types of computed data are as follows:

/ / function type
const getter = function () {
  this.firstName + this.lastName
}

/ / get/set type
const getter = function () {
  return this.index
}
Copy the code
  • Then in theSSRServer render case will be in_computedWatchersCreate a new oneWatcherThe instance. Taking the code above as an example,_computedWatchersAfter traversal, the following code can be used to express:
// The current VM instance
{
  _computedWatchers: {
    fullName: new Watcher(),
    active: new Watcher()
  }
}
Copy the code
  • Finally, the current traversal is judged firstcomputedIs it already invmInstance, if notdefineComputed()Method, if also need to determine the current traversalcomputedWhether andprops,dataName conflict, error if conflicting.Pay attention to: is currently traversed for the child componentcomputedHas been invmInstance, so it will not be calleddefineComputed()Method, as can be seen from the code comments above. For child components, trueinitComputedIs the process that takes place inVue.extendMethod:
Vue.extend = function (extendOptions) {
  // omit the code
  const Super = this
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub

  // Initialize computed for child components
  if (Sub.options.computed) {
    initComputed(Sub)
  }
}

// initComputed definition in extend.js
function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}
Copy the code

And then the defineComputed() method called by initComputed is the same method as the defineComputed() method now, It and the initComputed () method defined in the same location (SRC/core/instance/state. Js) :

const sharedPropertyDefinition = {
  enumerable: true.configurable: true.get: noop,
  set: noop
}
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 of the defineComputed() method is simple: assign values to get and set of sharedPropertyDefinition based on different types of computations. SharedPropertyDefinition is a shared configuration of the Object.defineProperty() method descriptor argument, which was introduced in proxy earlier.

In the SSR server rendering, sharedPropertyDefinition. The value is to call the get createComputedGetter () method, and in the case of SSR rendering of a service is invoked the createGetterInvoker () method. In the process of analyzing the Vue source code, the implementation of the createComputedGetter() method is analyzed next because of the focus on the Web browser side of the presentation. The createComputedGetter() method is defined in the same place as the defineComputed() method:

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

You can see that the createComputedGetter() method returns a function that is called when fetching computed, such as component rendering:

<template>
  <div>{{fullName}}</div>
</template>
Copy the code

The computedGetter() method looks like this: Component rendering gets the fullName calculation property and calls the computedGetter() method, which first evaluates the watcher.dirty property, This property is associated with the const computedWatcherOptions = {lazy: true} passed in when new Watcher(). In the constructor of the Watcher class, there is this code:

class Watcher {
  // omit the code
  constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    if (options) {
      this.lazy = !! options.lazy }else {
      this.lazy = false
    }
    this.dirty = this.lazy
  }
}
Copy the code

Because the lazy value is true, the watcher.dirty condition is judged to be true, and watcher.evaluate() is evaluated. Then, if dep.target is true, the dependency collection watcher.depend() will be covered in more detail in a later section. Just know that computed relies on a collection that is triggered when a component renders, and that collection is Render Watcher. Finally, take a look at the implementation of the Watcher.evaluate () method:

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

The evaluate() method is implemented very simply by triggering the getter for computed and setting dirty to false.

The computed update

Now that you’ve introduced the initialization of computed, take a look at the updating process for computed, using the following example:

export default {
  template: ` 
      
{{fullName}}
`
data () { return { total: 0.firstName: 'first'.lastName: 'last'}},computed: { fullName () { if (this.total > 0) { return this.firstName + this.lastName } else { return 'pleace click'}}},methods: { change () { this.total++ } } } Copy the code

Because total, firstName, and lastName are all responsive variables, when the fullName calculation attribute is initialized, the total value is 0, and the fullName calculation attribute has two Watcher, one is the calculation attribute Watcher. Another is rendering Watcher. When the button is clicked to trigger the event, the setter method for the total property fires, which in turn invokes a method called notify.

Set:function reactiveSetter (newVal) {
  / / to omit
  dep.notify()
}
Copy the code

Where notify() is a method defined in the Dep class:

export default class Dep {
   constructor () {
    this.subs = []
  }
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if(process.env.NODE_ENV ! = ='production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) = > a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Copy the code

Code analysis:

  • subsIt’s collectedwatcher, it’s an array, corresponding to the case above it’s an array of length 2 and one of them isrender watcher.
  • innotify()When a method is called, it is traversedsubsArray and then call the current in turnwatchertheupdateMethods. Among themupdateMethods are defined inWatcherClass with the following code:
class Watcher {
  // omit others
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}}Copy the code

When traversing for the first time, the watcher is the calculated property watcher, which was described earlier and whose this.lazy value is true, so this.dirty = true. When traversed the second time, the watcher is the render watcher, and for render watcher, its lazy value is false and this.sync is false, so queueWatcher() is called. There is no need to know how queueWatcher is implemented, except that the queueWatcher() method, when called, triggers updateComponent() :

updateComponent = () = > {
  vm._update(vm._render(), hydrating)
}
Copy the code

As you can see, the updateComponent() method calls the vm._update method, which re-renders the component. During the component rendering process, the fullName value is read again, which calls the following code:

fullName () {
  if (this.total > 0) {
    return this.firstName + this.lastName
  } else {
    return 'pleace click'}}Copy the code

FirstName + this.lastName = this.firstName + this.lastName = this.firstName + this.lastName = this.firstName = this.lastName Dependency collection is then performed. After the component rendering is completed, the dependency array subs of fullName will have four watcher, namely three calculated property watcher and one render watcher. No matter which value of the three calculated attributes watcher is updated, the above process is repeated, which is the process of computed update.

After analyzing the related processes of computed, the following flow chart can be obtained

Watch processing

Now that we’ve introduced the logic associated with handling computed, let’s look at how Watch handles it.

Watch the initialization

export function initState (vm: Component) {
  // omit the code
  const opts = vm.$options
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

NativeWatch is a constant defined in SRC /core/util/env.js:

// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch
Copy the code

Then, let’s take a look at initWatch implementation, it defined in SRC/core/instance/state. The js file:

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

As you can see, the implementation of the initWatch() method is very simple. We first check the watch and call createWatcher() if it is an array, or createWatcher() if it is not. According to the use rules of watch, we can write it in the following forms:

export default {
  data () {
    return {
      age: 23.name: 'AAA'.nested: {
        a: {
          b: 'b'}}}},watch: {
    name (newVal, oldVal) {
      console.log(newVal, oldVal)
    },
    nested: {
      handler (newVal, oldVal) {
        console.log(newVal, oldVal),
      },
      deep: true}}}Copy the code

Next, we need to look at the implementation of 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

Code analysis:

  • createWatcher()The main function of a method is to proceedwatchThe normalized parameters are then passed tovm.$watch().
  • increateWatcher()In the first judgmenthandlerWhether the parameter is a common object, if it is a common object, it is defined in the following formatwatch:
{
  watch: {
    nested: {
      handler (newVal, oldVal) {
        console.log(newVal, oldVal),
      },
      deep: true}}}Copy the code

At this point, you should assign handler to the optional options parameter, and then handler assigns the actual callback function.

  • Then, thehandlerA type judgment is made if yesstringType takes this timevm[handler]Assign to it. According to the logic of this code, it means that we can choose to putwatchThe callback function is defined inThe methods of:
export default {
  data () {
    return {
      name: 'AAA'}},watch: {
    name: 'nameWatchCallback'
  },
  methods: {
    nameWatchCallback (newVal, oldVal) {
      console.log(newVal, oldVal)
    }
  }
}
Copy the code
  • Finally, the normalized parameters are passed tovm.$watch(). about$watch()When to mount toVue.prototypeAnd, as we’ve already shown before, it happens instateMixinIn the.

$watch(); $watch();

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

As you can see, the $watch method does two main things: create a Watcher instance and return the unwatchFn function. Let’s explain the logic of each part in detail.

Create Watcher instance

Let’s first look at the code for the Watcher constructor:

// Simplify the code
class Watcher {
  constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    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}}}Copy the code

As we can see from the constructor, when a watch is instantiated, the deep, user, lazy, sync, and before properties are processed based on the options passed. Watcher has several different classifications depending on how it is used:

  • render watcherRender:watcher, such as when intemplateUse in templates{{}}When the syntax reads a variable, the dependencies that the variable collects arerender watcherRaised when the value of the variable is updatedrender watcherRe-render the component. Render or notwarcher, using the constructor argumentisRenderWatcherfortrueMake a distinction.
  • computed watcher: Compute attributeswatcherWhen we define the calculated attribute, the dependence of the calculated attribute collection is another one or more variables. When the value of one of the variables is changed into a variable, the calculated attribute will be triggered for re-evaluation. Whether it is a calculated propertywatcher, the use ofoptions.lazyfortrueMake a distinction.
  • user watcher: User-definedwatcherMost of the timethis.$watchOr componentswatchIn a selection configuration, the dependency collected is the variable itself, which is called when the value of the variable changeswatchThe provided callback function. Whether it is user-definedwatcher, the use ofoptions.userfortrueMake a distinction.

Return the unwatchFn function

In the constructor, you can see that it defines a _watchers variable and then adds itself to the array each time it is instantiated to make it easier to clear dependencies. In the previous introduction, we learned that $watch returns an unwatchFn function, which is used to cancel listening. Next, look at the concrete implementation of the teardown() method.

// The Watcher class simplifies code
class Watcher {
  constructor () {
    this.active = true
    this.deps = []
  }
  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}}}// Dep class compact code
class Dep {
  constructor () {
    this.subs = []
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
}
Copy the code

The teardown() method is simply implemented to remove the current Watcher from the DEPS array, where Dep instances are stored.