First published at: github.com/USTB-musion…

Vue’s component objects support two options, computed and watch, but what are the differences between the two attributes and their underlying implementation principles? This article will use examples and source code to summarize.

This article will be summarized from the following six modules:

  • Computed and Watch definitions
  • There are similarities and differences between computed and watch
  • Advanced use of watch
  • The nature of computed — Computed Watch
  • How does watch work at the bottom?
  • conclusion

Computed and Watch definitions

1.computed is a computational attribute, which is similar to a filter. It processes the data bound to the view, listens for changes and then executes corresponding methods. If you are not sure about this part, you can take a look at the principle of responsive system in vue.js, another article of mine. Examples from the official website:

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
Copy the code
var vm = new Vue({
  el: '#example'.data: {
    message: 'Hello'
  },
  computed: {
    // Calculates the getter for the property
    reversedMessage: function () {
      // 'this' points to the VM instance
      return this.message.split(' ').reverse().join(' ')}}})Copy the code

Results:

Original message: "Hello"
Computed reversed message: "olleH"
Copy the code

Computed properties are cached based on their dependencies. They are reevaluated only if the dependencies change. It is important to note that “reversedMessage” cannot be defined in the props and data of the component, otherwise an error will be reported.

2. Watch is a listening action to observe and respond to data changes on a Vue instance. Examples from the official website:

<div id="watch-example">
  <p>
    Ask a yes/no question:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>
Copy the code
<! Because the ecosystem of AJAX libraries and common tools is already quite rich, the Vue core code is not duplicated --> <! Provide these features to keep things lean. This also gives you the freedom to choose the tools you're more familiar with. --><script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
  el: '#watch-example'.data: {
    question: ' '.answer: 'I cannot give you an answer until you ask a question! '
  },
  watch: {
    // If 'question' changes, the function is run
    question: function (newQuestion, oldQuestion) {
      this.answer = 'Waiting for you to stop typing... '
      this.debouncedGetAnswer()
    }
  },
  created: function () {
    // '_. Debounce' is a function that limits the frequency of operations through Lodash.
    // In this case, we want to limit the frequency of access to yesNo. WTF/API
    // The AJAX request is not sent until the user has entered. Want to learn more about
    // '_. Debounce' functions (and their relatives' _. Throttle '),
    // Please refer to https://lodash.com/docs#debounce
    this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)},methods: {
    getAnswer: function () {
      if (this.question.indexOf('? ') = = = -1) {
        this.answer = 'Questions usually contain a question mark. ; -) '
        return
      }
      this.answer = 'Thinking... '
      var vm = this
      axios.get('https://yesno.wtf/api')
        .then(function (response) {
          vm.answer = _.capitalize(response.data.answer)
        })
        .catch(function (error) {
          vm.answer = 'Error! Could not reach the API. ' + error
        })
    }
  }
})
</script>
Copy the code

In this example, using the Watch option allows us to perform an asynchronous operation (accessing an API), limit how often we perform that operation, and set the intermediate state until we get the final result. These are all things you can’t do with computed properties.

There are similarities and differences between computed and watch

Here’s a summary of the similarities and differences:

Same: Computed and Watch both listen to/rely on a piece of data and process it

Differences and similarities: Both of them are actually implementations of vUE’s listeners. However, computed is mainly used to process synchronous data, while Watch is mainly used to observe changes in a value to complete an expensive and complex business logic. Use computed first when you can, avoiding the awkward situation of calling Watch multiple times when multiple data affects one of them.

Advanced use of watch

1. The handler method and immediate attribute

<div id="demo">{{ fullName }}</div>
Copy the code
var vm = new Vue({
  el: '#demo'.data: {
    firstName: 'Foo'.lastName: 'Bar'.fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      console.log('Not executed the first time ~')
      this.fullName = val + ' ' + this.lastName
    }
  }
})
Copy the code

As you can see, watch will not execute during initialization. In the example above, listen calculations are performed only when the value of firstName changes. But what if you want to execute it the first time it’s bound? It’s time to modify our example:

  watch: {
    firstName: {
      handler(val) {
        console.log('Executed for the first time ~')
        this.fullName = val + ' ' + this.lastName
      },
      // execute the handler method immediately after declaring firstName in watch
      immediate: true}}Copy the code

Open the console and you will see that ‘first time executed ~’ is printed. Notice that we’ve attached a handler method to firstName. The watch method we wrote earlier actually writes this handler by default. Vue.js will handle this logic, and it will compile to this handler.

Immediate :true means that if firstName is declared in wacth, the handler method is executed immediately. If firstName is false, the handler method is not executed at binding time. Why does the addition of the handler method and immediate:true execute the first time on the binding? You’ll understand that when you analyze the source code.

2. Deep properties

Watch also has a deep property that indicates whether deep listening is enabled. The default value is false.

<div id="app">
  <div>obj.a: {{obj.a}}</div>
  <input type="text" v-model="obj.a">
</div>
Copy the code
var vm = new Vue({
  el: '#app'.data: {
    obj: {
    	a: 1}},watch: {
    obj: {
      handler(val) {
       console.log('obj.a changed')},immediate: true}}})Copy the code

When we enter data in the input field to change the value of obj.a, we find that ‘obj.a changed’ is not printed on the console. Restricted by modern JavaScript (and deprecated Object.Observe), Vue cannot detect additions or deletions of Object attributes. Since Vue performs getter/setter conversion procedures on the property when it initializes the instance, the property must exist on the Data object for Vue to convert it, and for it to be responsive.

By default, this handler only listens for changes in the reference to obj. This handler only listens for changes in the reference to obj when we reassign obj to the Mounted hook:

mounted() {
  this.obj = {
    a: '123'}}Copy the code

The handler will then execute and print ‘obj.a changed’.

But what if we need to listen for property values in OBj? This is where the deep property comes in handy. All we need to do is add deep:true to listen deeply for obj attributes.

  watch: {
    obj: {
      handler(val) {
       console.log('obj.a changed')},immediate: true.deep: true}}Copy the code

The deep property means deep traversal, traversing the object layer by layer, adding listeners to each layer. In the source code, is defined in SRC/core/observer/traverse by js:

/* @flow */

import { _Set as Set, isObject } from '.. /util/index'
import type { SimpleSet } from '.. /util/index'
import VNode from '.. /vdom/vnode'

const seenObjects = new Set(a)/** * Recursively traverse an object to evoke all converted * getters, so that every nested property inside the object * is collected as a "deep" dependency. */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if((! isA && ! isObject(val)) ||Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

Copy the code

If this.deep == true, deep is present, then the dependency of each deep object is triggered to track its changes. Traverse methods traverse each object or array recursively, triggering their getters so that each member of the object or array is collected by a dependency, forming a “deep” dependency. There is also a minor optimization in this function implementation, which records child responsive objects to seenObjects with their dep.ID during traversal to avoid repeated access later.

But using the deep attribute adds listeners to each layer, and the performance overhead can be significant. So we can optimize it as a string:

  watch: {
    'obj.a': {
      handler(val) {
       console.log('obj.a changed')},immediate: true
      // deep: true}}Copy the code

Until the ‘obj. A ‘property is encountered, a listener is set for that property to improve performance.

The nature of computed — Computed Watch

We know that new Vue() calls the _init method, which initializes the life cycle, initializes events, initializes render, initializes data, computed, methods, wacther, and so on. If you are not sure about this part, please refer to another article I wrote: vue.js source perspective: Dissecting the template and the process of rendering the data into the final DOM. Today is mainly to see the following initialization watch (initWatch) implementation, I added notes easy to understand, is defined in SRC/core/instance/state. In js:

// An object used to pass in an instance of Watcher, namely computed Watcher
const computedWatcherOptions = { computed: true }

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  // Declare a Watchers and mount it to the VM instance
  const watchers = vm._computedWatchers = Object.create(null)
  // In SSR mode, computed attributes fire only getter methods
  const isSSR = isServerRendering()

  // Iterate over the computed method passed in
  for (const key in computed) {
    // Take each method in a computed object and assign it to userDef
    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 it is not SSR server rendering, create a Watcher instance
    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)) {
      // If the key in computed is not set in the VM, mount it via the defineComputed function
      defineComputed(vm, key, userDef)
    } else if(process.env.NODE_ENV ! = ='production') {
      // If data and props have the same name as key in computed, a warning is generated
      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

The source code identifies an empty object called Watchers, which is also mounted on the VM. It then iterates through the evaluated properties and assigns the method for each property to userDef, or getter if userDef is function, and then determines if it is server rendered, and creates an instance of Watcher if it is not. Note, however, that we passed in a fourth parameter, computedWatcherOptions, in our new instance. Const computedWatcherOptions = {computed: true}, which is the key to implementing computedWatcher. At this point, the logic in Watcher changes:

    / / source defined in SRC/core/observer/watcher. Js
    // options
    if (options) {
      this.deep = !! options.deepthis.user = !! options.userthis.computed = !! options.computedthis.sync = !! options.syncthis.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    // Other codes......
    this.dirty = this.computed // for computed watchers
Copy the code

The options passed in here are the computedWatcherOptions defined above. When you go to initData, options do not exist, but when you go to initComputed, Computed in computedWatcherOptions is true. Note the line this.dirty = this. puted, which assigns this. puted to this.dirty. Now look at the following code:

  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }
Copy the code

Only if this.dirty is true can it be evaluated by this.get() and then set this.dirty to false. During evaluation, value = this.getter.call(vm, vm) is called, which is essentially the getter function defined for the evaluated property, otherwise value is returned.

When a change is made to the calculated property dependent data, the setter procedure is triggered to notify all watcher updates subscribed to the change, executing the watcher.update() method:

  /** * Subscriber interface. * Will be called when a dependency changes. */
  update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() = > {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}Copy the code

For computed watcher, it actually has two modes, lazy and active. If this.dep.subs.length === 0 is true, then no one is subscribing to this computed watcher change, so this.dirty = true will be reevaluated only the next time the computed property is accessed. Otherwise the getAndInvoke method is executed:

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

The getAndInvoke function recalculates and then compares the old and new values in three cases (1). If the value is an object or array and the deep property is set, the callback function is this.dep.notify(). In this case, watcher rerenders. This explains that the computed attributes described in the official website are cached based on their dependencies.

How does watch work at the bottom?

As mentioned above, the _init method is called on new Vue() to complete the initialization. In the call of the initWatch method, defined in 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

Iterate over the watch object and assign each watch[key] to handler, iterating over the movie createWatcher method if it is an array, otherwise call createWatcher directly. Now look at the definition of the createWatcher method:

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

The createWatcher method vm.? Watch (keyOrFn, handler, options) function, call the Vue. Prototype. $watch method, defined in SRC/core/instance/state in js:

  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) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
Copy the code

As you can see from the code, watch will eventually call the vue.prototype. watch method, which will call createWatcher if cb is an object. The createWatcher method is called because of the watch method, which first checks if cb is an object and calls createWatcher because the watch method can be called directly by the user. It can pass an object or a function. Next, const watcher = new watcher (VM, expOrFn, cb, options) instantiates a new watcher, note that this is a user watcher, Because options.user = true. By instantiating Watcher, the watch will eventually execute watcher’s run method as soon as its data is sent, executing the cb callback, and if we set immediate to true, the cb callback will be executed. That is, if the immediate attribute is set to true, the watch is bound for the first time. Finally, an unwatchFn method is returned that calls the tearDown method to remove the watcher.

So how does Watcher work? It’s also based on Watcher essentially, which is a user Watcher. I mentioned earlier that computed property computed is essentially a computed watcher.

conclusion

Through the above analysis, we have an in-depth understanding of how computed property and listen property watch work. The computed property is essentially a computed watch, and the listening property is essentially a user watch. In fact, both of them are implementations of vUE’s listeners. However, computed is mainly used to process synchronous data, while watch is mainly used to observe the change of a value to complete an expensive and complex business logic. Use computed first when you can, avoiding the awkward situation of calling Watch multiple times when multiple data affects one of them.

You can pay attention to my public account “Muchen classmates”, goose factory code farmers, usually record some trivial bits, technology, life, perception, grow together.