Recently, several students came to me to discuss about computed in VUE. They generally felt that the logic of computed in VUE was a little convoluted, but they did not find a detailed explanation on the Internet, or they did not find a detailed explanation on the point they did not understand. After speaking for several times, I think it can be sorted out and written, and we can take it as a part of the reference, hoping to help you solve your doubts. If it is helpful to you, please give me a thumbs-up ~ after all, pure hand playing is not easy, ha ha.

Start the text.

In this article, I will not talk about how the data we define in data is processed at the bottom level. If you are confused, please refer to other articles. Go straight to computed (this series will still be about VUE2).

To understand computed underbelly, you need to start where computed started: initState(VM) to find initComputed method definitions.

function initComputed (vm: Component, computed: Object) {
  
  const watchers = vm._computedWatchers = Object.create(null)

  for (const key in computed) {
    const userDef = computed[key]
    // Getters are user-defined calculation functions
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    
    / /...
      // Initialize a watcher for each computed attribute, and pass {lazy: true}, for example; vm._computedWatchers['someComputed'] = new Watcher({... }); Lazy means it doesn't need to be executed right away.
      // And pass in the custom calculation function
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
  
    if(! (keyin vm)) {
      // During initialization, the key in computed is not mounted to the VM, so the following logic is executed:
      // Mount the computations written by the real user
      // userDef -> compute function
      defineComputed(vm, key, userDef)
    }
  }
}

Copy the code

Ok, the main idea of this paragraph is:

  1. Iterate through the computed attributes defined by our developers, pass the function of each attribute to Wacther and instantiate it, and mount the WATcher to VVM._computedWatchers, which will be used frequently later, as a carrier.
  2. DefineComputed () is executed to process the calculation function and mount it to the VM via Object.defineProperty.

Okay, look at defineComputed() in detail

I’m going to simplify the code, just leave the main flow, all the boundary conditions and so on out here.

const sharedPropertyDefinition = {
  enumerable: true.configurable: true.get: noop,
  set: noop
}
// ...

export function defineComputed() {
    // ... 
    if (typeof userDef === 'function') {
        // will enter here because userDef is a computed function written by the developer
        sharedPropertyDefinition.get = shouldCache
               
              ? createComputedGetter(key)
              : createGetterInvoker(userDef)
            sharedPropertyDefinition.set = noop
    }

    if(process.env.NODE_ENV ! = ='production' &&
          sharedPropertyDefinition.set === noop) {
          // Tell developers that computed does not have setters, that is, computed properties cannot perform setter operations
        sharedPropertyDefinition.set = function () {
          warn(
            `Computed property "${key}" was assigned to but it has no setter.`.this)}}// Mount a computed key to the VM so that you can access it directly in your code later using this. XXX
      Object.defineProperty(target, key, sharedPropertyDefinition)
}


Copy the code

CreateComputedGetter must be execution related, such as when the calculation property is called in the

    new Vue({
        ...
        computed: {
            // Do not write
            initComputed() {
                return This is the initial calculation.
            },
            // Normal condition
            normalComputed() {
                return this.aa + this.bb + 'This is normal.'}}})Copy the code

From the above pseudo-code, the initComputed function returns a static string, which is relatively simple, but if so, you don’t need to use computed properties.

Aa and this.bb are also referred to in our function. When either of these variables changes, we have to update the calculation function and then update the page view. Is that right?

So with that in mind, let’s look at what this createComputedGetter does to the implementation of this function.

Okay, so let’s continue with the createComputedGetter function

function createComputedGetter (key) {
  // this is executed when the value of the calculated property is used.
  return function computedGetter () {
    // Remember how we bound computed to this._computedWatchers?
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      
      // If our computed function references other variables, dirty must be true when initialized, and will eventually be set to true when the referenced variables change.
      // When dirty is true, the data is dirty and should be updated
      if (watcher.dirty) {
        // So it executes, and then executes, the calculation function defined by the developer.
        watcher.evaluate()
      }
      // Then dep. target is actually another watcher at the top of the stack, depending on the execution of the business code, possibly the render function
      if (Dep.target) {
        // To execute a function such as a calculation,
        // 1. Push the current watcher into a Dep stack, and then update the dep. target to the current watcher.
        // 2. Execute the getter function in watcher,
        // 3. After executing, the current watcher is removed from the stack, and dep. target is redirected to another watcher at the top of the stack
        // 4. Return the result of the computed function

        // What does watcher depend do? So let's take a closer look
        watcher.depend()
      }
      // Return value
      return watcher.value
    }
  }
}

Copy the code

Watcher.evaluate () : evaluate() You need to focus on this function. Evaluate: Whether or not watcher should be updated.

  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  // See what the get function does.
  get () {
    
    /** * export function pushTarget (target: ? Watcher) { targetStack.push(target) Dep.target = target } */
    // pushTarget and popTarget are both useful, but more on that later
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // Executes a computed function defined by the developer
      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
  }

Copy the code

Aside from pushTarget and popTarget, this seems to allow the developer-written function to execute, and you probably don’t feel that special. But I believe that the question is, how does computed power work?

I can reliably tell you that pushTarget and popTarget are very important

Now, with a little doubt, let’s first execute the calculation function written by the developer:

    normalComputed() {
        return this.aa + 'This is normal.'
    }

Copy the code

In this case, this.aa will be executed. If you have not learned initData, you are advised to learn more about the data function implementation (by default, everyone writes functions).

Ok, let’s go to this.aa logic

Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // Check who the dep. target is and, in general, which watcher calls data
      // dep. target is the watcher. Remember I mentioned a bunch of pushTarget functions that are really, really important

      // How do we notify computed- Watcher of changes in this data?
      // Dep. Depend calls the addDep method of watcher, which assigns the dep object to watcher's newDep[]. The watcher instance is reversely assigned to subs[] of the DEP instance
      // Here's the proof:
      /* addDep (dep: Dep) { //.... if (! This.depids. has(id)) {// here!! dep.addSub(this) } } */

      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if(getter && ! setter)return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // If the newly changed data becomes non-basic, observe again. Otherwise, you cannot be responsivechildOb = ! shallow && observe(newVal)// Notification update
      dep.notify()
    }
  })

Copy the code

Now, with my very detailed explanation in this code, does that make sense to you? Let me be clear:

export function pushTarget (target: ? Watcher) {
  targetStack.push(target)
  Dep.target = target
}

Copy the code

PushTarget sets dep. target to the current watcher, which is our current normalComputed Watcher. So, when the getter above executes, it assigns the current Dep instance to the watcher. NewDeps array, and it assigns the current Watcher back to the Dep instance. The Dep instance is bound to this.aa in the example. As the comments in the code say, we have enough evidence:

// dep.depend(), pointing to the depend function
/ / dep. Js
depend () {
    if (Dep.target) {
      // Dep.target is a watcher, no doubt about it
      Dep.target.addDep(this)}}// Then go to the watcher
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // Push deP into Watcher's newDeps array
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
      // See! There you go! This is the main evidence I'm talking about!!
      // Put watcher back into the Dep instance
        dep.addSub(this)}}}Copy the code

At this point, IT is necessary for me to summarize the stages, in case you just understand the point, and confused, this logic is really more convoluted, but also more clever.

  • This DEP is reactiveaaA property in
  • Executes when executing the developer’s evaluation functionthis.aaAnd then into the reactive logic of AA
  • To reactive logicDep.targetThat is, computed Watcher today, assigns DEP to watcher’s newDeps property
  • At the same time, the watcher is reversely assigned to dep.sub
  • This creates a two-way relationship. When we perform a computed function, we store the DEP whenthsi.aaChanges, will execute in the setterdep.notify()To execute watcher in this.sub.
  • Once again, the computed function is notified, the function executes again, and the view is updated again.

So, does that make sense?

I hope this short article can help you. The next article may analyze the bottom layer of Watch, but it may be updated slowly because it is busy. Stay tuned