Review the use of watch

Watch is a method in Vue to listen for changes in data. Let’s review the use of Watch before reading the source code

Listen for basic data types

<div>
  {{ name }}
  <button @click="changeName">Change the name</button>
</div>
Copy the code
export default {
  data() {
    return {
      name: 'maoxiaoxing',}},watch: {
    name(val, oldval) {
      console.log(val, oldval)
    }
  },
  methods: {
    changeName() {
      this.name = this.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'}}}Copy the code

Watch can accept two parameters, one after the change and one before the change, and you can process some logic based on these two values

Listening to the object

<div>
  {{ obj.name }}
  <button @click="changeName">Change the name</button>
</div>
Copy the code
export default {
  data() {
    return {
      obj: {
        name: 'maoxiaoxing',}}},watch: {
    obj: {
      handler(val, oldval) {
        console.log(val, oldval)
      },
      deep: true.immediate: true,}},methods: {
    changeName() {
      this.obj.name = this.obj.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'}},created() {
    console.log('created')}}Copy the code

When the listening object changes, add the “deep” attribute to deep listening object data; If you want to execute the watch method when the page comes in, just add immediate. It is important to note that the watch with the immediate property is executed before the created life cycle

Watch takes an array as an argument

When I look at the Vue source code, I find it interesting that if the properties that watch listens to receive an array instead of setting a method, I can pass multiple methods to the currently listening properties

export default {
  data() {
    return {
      name: 'jack',}},watch: {
    name: [{handler: function() {console.log(1)}, immediate: true },
      function(val) {console.log(val, 2)}}],methods: {
    changeName() {
      this.name = this.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'}}}Copy the code

The array can take different forms of arguments, either methods or objects, in the same way as a normal watch. Can receive data as parameters this point is not found in the official documentation, as to why can write so, the following source code will be mentioned.

Initialize the watch

initState

// src\core\instance\state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // If there are props, initialize them
  if (opts.methods) initMethods(vm, opts.methods) // If there are methods, initialize the methods in the methods
  if (opts.data) { // Initialize, data, if any; Otherwise it responds with an empty object
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed) // If computed, initialize computed
  if(opts.watch && opts.watch ! == nativeWatch) {// Initialize the watch if there is one
    initWatch(vm, opts.watch)
  }
}
Copy the code

First initialize the watch in initState, passing the watch property to the initWatch method if it has one

initWatch

// src\core\instance\state.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

InitWatch iterates over the watch and determines whether each value is an array. If it is an array, it iterates over the array and creates multiple callbacks. This explains why the watch listens to an array. If it’s not an array, create the callback function directly. Here we can also see the benefits of learning the source code. By learning the source code, we can learn some writing methods that are not mentioned in official documents.

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

CreateWatcher will determine if handler is an object. If handler is an object, mount it to the options property and extract the handler property from the object. If handler is a string, this method will be found from the Vue instance and assigned to handler. As you can see here, watch also supports writing strings. Execute the $watch method on the Vue instance.

$watch

Vue.prototype.$watch = function (
  expOrFn: string | Function, cb: any, options? :Object
) :Function {
  // Get the Vue instance this
  const vm: Component = this
  if (isPlainObject(cb)) {
    // Execute createWatcher if cb is an object
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  // Mark user watcher
  options.user = true
  // Create user watcher object
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // Judge immediate if true
  if (options.immediate) {
    // Immediately execute the cb callback and pass in the current value
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}// Return the method to cancel listening
  return function unwatchFn () {
    watcher.teardown()
  }
}
Copy the code

The watch function is an instance method of Vue, which means that we can use Vue. The watch function is an instance method of Vue, which means that we can use Vue. The official documentation is very detailed. $watch creates a Watcher object. This also involves the reactive principle. Data changed in the watch can be changed in a responsive way. It also determines if the immediate attribute exists and, if so, calls the callback directly.

Watcher

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    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
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // expOrFn is a string, for example watch: {'person.name': function... }
      // parsePath('person.name') returns a function that gets the value of person.name
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop process.env.NODE_ENV ! = ='production' && warn(
          `Failed watching path: "${expOrFn}"` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /** * Evaluate the getter, and re-collect dependencies. */
  /* Get the value of the getter and redo the dependency collection */
  get () {
     /* Set your own watcher observer instance to dep. target to rely on the collection. * /
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      /* Getters are performed to render, but dependencies are collected. After setting dep. target to its own observer instance, the getter operation is performed. For example, there may be a, B, and C data in the current data, and the getter rendering needs to depend on a and C, so the getter function of a and C data will be triggered when the getter is executed. In the getter function, the dep. target can be determined whether the Dep. Target exists and then the dependency collection is completed. Place the observer object in the subs of Dep in the closure. * /
      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 deep exists, trigger the dependency of each deep object, tracking its changes */
      if (this.deep) {
        /* Recurse each object or array, triggering their getters so that each member of the object or array is collected by dependencies, forming a "deep" dependency */
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    returnvalue } ... Other methods}Copy the code

I left out some of the other methods in the Watcher above, except for the get function, which we can see in the get function that if there is a deep property, it will recursively process every property in the object to achieve the effect of deep listening. Here is the end of the explanation of the use and principle of Watch. By reading the source code, we can not only understand how the Vue framework is implemented internally, but also see some usages not mentioned in the official documents, which is very helpful to us.