Editor’s note: From time to time, we invite engineers to talk about interesting technical details in the hope that knowing why will help you perform better in an interview. It also gives the interviewer more ideas.

Although the current technology stack has been transferred from Vue to React, the actual experience of several projects developed using Vue before is very pleasant. Vue documents are clear and standardized, API design is simple and efficient, friendly to front-end developers, and easy to use. I even think it is more efficient to use Vue than React in many scenarios. I have read the source code of Vue on and off before, but I haven’t summarized it. Therefore, I will make some technical summaries here to deepen my understanding of Vue. So today I’m going to write about the implementation of computed, one of the most commonly used apis in Vue.

Basic introduction

Without further ado, a basic example is as follows:

<div id="app">
    <p>{{fullName}}</p>
</div>
Copy the code
new Vue({
    data: {
        firstName: 'Xiao'.lastName: 'Ming'
    },
    computed: {
        fullName: function () {
            return this.firstName + ' ' + this.lastName
        }
    }
})
Copy the code

In Vue we don’t need to evaluate {{this.firstName + “+ this.lastName}} directly in the template, because putting too much declarative logic in the template would make the template too heavy, especially if the page uses a lot of complex logical expressions to process data. This has a big impact on the maintainability of a page, and computed is designed to solve this problem.

Contrast listenerwatch

Of course, a lot of times when we use computed, we compare it to another API in Vue, the watch listener, because in some ways it’s the same, it’s based on Vue’s dependency tracking mechanism, and when a dependency data changes, All related data or functions that depend on this data are automatically changed or called.

While computing properties is more appropriate in most cases, sometimes a custom listener is required. That’s why Vue provides a more generic way to respond to changes in data with the Watch option. This approach is most useful when asynchronous or expensive operations need to be performed when data changes.

As we can see from the official Vue documentation explaining watch, using the Watch option allows us to perform asynchronous operations (accessing an API) or high-performance operations, limits how often we can perform that operation, and sets intermediate states before we get the final result, all of which cannot be done by computing properties.

The following also summarizes several points aboutcomputedwatchDifference:

  1. computedIs to evaluate a new attribute and mount it to the VM (Vue instance), whilewatchYes The listener already exists and is mounted tovmThe data of thewatchYou can also listen incomputedCalculate property changes (among othersdata,props)
  2. computedEssentially a lazy-evaluated observer, cacheable only when the dependency changes after the first accesscomputedProperty, the new value will be computed, andwatchThe execution function is called when the data changes
  3. In terms of usage scenarios,computedApplies to one data affected by multiple data, whilewatchApply one data to affect multiple data;

Above, we have seen some differences between computed and Watch and the differences in usage scenarios. Of course, sometimes the two are not so clear and strict, and finally, we need to analyze them in different businesses.

The principle of analysis

To get back to computed, the topic of this article, let’s take a closer look at how it works in the Vue source code.

Before looking at computed source code, we need to have a basic understanding of Vue’s responsive system, which Vue calls a non-invasive responsive system. The data model is just plain old JavaScript objects, and the view updates automatically when you modify them.

When you pass a normal JavaScript Object to the Vue instance’s data option, Vue iterates through all of the Object’s properties and converts them into getters/setters using Object.defineProperty. These getters/setters are invisible to the user, but internally they let Vue track dependencies and notify changes when properties are accessed and modified. Each component instance has a corresponding Watcher instance object, which records properties as dependencies during component rendering. Later, when the setter for the dependency is called, Watcher is told to recalculate, causing its associated component to be updated.

Vue response system has three core points: Observe, Watcher and DEP:

  1. observe: traversaldataProperty in theObject.defineProperty 的 get/setMethods Data hijacking was carried out.
  2. dep: Each property has its own message subscriberdepIs used to hold all observer objects subscribed to the property.
  3. watcher: Observer (object), passdepThe implementation of the response properties to listen, listen to the result, actively trigger their own callback to respond.

With a basic understanding of responsive systems, let’s look at computational properties. First we find calculate attribute is initialized in the SRC/core/instance/state. The js file initState function

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 initialization
  if (opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

Call the initComputed function (which also initializes initData and initWatch, respectively) and pass in two parameters, the VM instance and the computed option defined by the opt.computed developer, to initComputed:

const computedWatcherOptions = { computed: 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

Starting with this code, let’s look at the following sections:

  1. Gets the definition userDef and getter evaluation functions for the calculated property

    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    Copy the code

    There are two ways to define a calculated property: one is to add a function directly, and the other is to add the object form of set and get methods, so we first get the definition of the calculated property userDef, and then get the corresponding getter function according to the type of userDef.

  2. Calculate the observer watcher and message subscriber DEP for the properties

    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
    )
    Copy the code

    The reference to vm._computedWatchers object contains watcher instances of each calculated attribute. The watcher constructor takes four arguments to instantiate the watcher constructor: The vm instance, getter evaluation function, noOP empty function, computedWatcherOptions constant object (here Watcher is given an identifier {computed:true} indicating that this is a computed property and not a non-computed observer, We come to the definition of the Watcher constructor:

    class Watcher {
      constructor( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) {if (options) {
          this.computed = !! options.computed }if (this.computed) {
          this.value = undefined
          this.dep = new Dep()
        } else {
          this.value = this.get()
        }
      }
      
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          value = this.getter.call(vm, vm)
        } catch (e) {
          
        } finally {
          popTarget()
        }
        return value
      }
      
      update () {
        if (this.computed) {
          if (this.dep.subs.length === 0) {
            this.dirty = true
          } else {
            this.getAndInvoke((a)= > {
              this.dep.notify()
            })
          }
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      evaluate () {
        if (this.dirty) {
          this.value = this.get()
          this.dirty = false
        }
        return this.value
      }
    
      depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
    }
    Copy the code

    For brevity and focus, I’ve manually removed the code snippets that we don’t need to worry about for now. Look at Watcher’s constructor, using the fourth parameter {computed:true} passed in by new Watcher, For evaluating the property, Watcher executes the if condition this.dep = new dep (), which is the message subscriber that created the property.

    export default class Dep {
      statictarget: ? Watcher; subs:Array<Watcher>;
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
    
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
    
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
      notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    
    Dep.target = null
      
    Copy the code

    Dep also simplifies some of the code. Let’s look at the relationship between Watcher and Dep and summarize it in one sentence

    The DEP is instantiated in watcher and subscribers are added to dep.subs, which notifies each Watcher of updates through notify through dep.subs.

  3. DefineComputed Defines compute attributes

    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

    Because computed attributes are mounted directly to instance objects, you need to determine whether an object already has a property with the same name before defining it. DefineComputed passes in three parameters: the VM instance, the key to calculate the property, and the definition of the calculated property (object or function) by userDef. Then continue to find the defineComputed definition:

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

    At the end of this code, the native Object.defineProperty method is called, where the third argument passed in is the property descriptor sharedPropertyDefinition, initialized as:

    const sharedPropertyDefinition = {
      enumerable: true.configurable: true.get: noop,
      set: noop
    }
    Copy the code

    The get/set method of sharedPropertyDefinition is overwritten after userDef and shouldCache. The get function of sharedPropertyDefinition is the result of createComputedGetter(key), We find the createComputedGetter call result and finally rewrite the sharedPropertyDefinition to look something like this:

    sharedPropertyDefinition = {
        enumerable: true.configurable: true.get: function computedGetter () {
            const watcher = this._computedWatchers && this._computedWatchers[key]
            if (watcher) {
                watcher.depend()
                return watcher.evaluate()
            }
        },
        set: userDef.set || noop
    }
    Copy the code

    The get access function is executed when the calculated property is called to associate the watcher with the observer object and then execute Wather.depend () to collect the dependency and watcher.evaluate() to evaluate.

After analyzing all the steps, let’s summarize the whole process:

  1. When the component is initialized,computeddataWill build their own response systems,ObservertraversedataFor each property set inget/setData interception
  2. Initialize thecomputedWill be calledinitComputedfunction
    1. Sign up for awatcherInstance, and instantiate one insideDepMessage subscribers are used for subsequent collection dependencies (such as for rendering functions)watcherOr something else to observe changes in the properties of the calculationwatcher
    2. This is triggered when the calculated property is invokedObject.definePropertythegetAccessor function
    3. callwatcher.depend()Method to its own message subscriberdepsubsTo add additional attributeswatcher
    4. callwatcherevaluateMethod (then calledwatchergetLet yourself be something elsewatcherSubscribers to the message subscriber will firstwatcherAssigned toDep.targetAnd then executegetterEvaluation functions, when accessing properties inside the evaluation function (such as fromdata,propsOr othercomputed), will also trigger themgetThe accessor function then computes the propertywatcherAttribute added to the evaluation functionwatcherMessage subscriber todepWhen these operations are complete, they are finally closedDep.targetAssigned tonullAnd returns the result of the evaluation function.
  3. Trigger when a property changessetIntercepting the function and then calling its own message subscriberdepnotifyMethod to traverse the currentdepHolds all subscribers inwathcersubsArray and call them one by onewatcherupdateMethod to complete the response update.

The text/versa

A coder yearning for poetry and the distance

Sound/fluorspar

This article has been authorized by the author, the copyright belongs to chuangyu front. Welcome to indicate the source of this article. Link to this article: knownsec-fed.com/2018-09-12-…

To subscribe for more sharing from the front line of KnownsecFED development, please search our wechat official account KnownsecFED. Welcome to leave a comment to discuss, we will reply as far as possible.

Thank you for reading.