Source code analysis vue Watch listener

Before reading the listener source code, it is recommended to take a look at the source code for responsivity analysis of the VUE responsivity principle (see this if you can’t open it).

Several uses of listeners

<div id="app">
    <input v-model="text1">
    <input v-model="text2">
    <input v-model="text3.val">
    <input v-model="text4.val">
</div>
Copy the code
var vm = new Vue({
    el: '#app'.data: {
        text1: 'Hello 1'.text2: 'Hello 2'.text3: {
            val: 'Hello 3',},text4: {
            val: 'Hello 4',}},watch: {
        text1: 'fun'.text2: function(newVal, oldVal){
            console.log('newVal:', newVal)
            console.log('oldVal:', oldVal)
        },
        'text3.val': {
            handler: function(newVal, oldVal){
                console.log('newVal:', newVal)
                console.log('oldVal:', oldVal)
            },
            deep: true.immediate: true,},text4: [
            'fun'.function(newVal, oldVal){
                console.log('newVal:', newVal)
                console.log('oldVal:', oldVal)
            },
            {
                handler: function(newVal, oldVal){
                    console.log('newVal:', newVal)
                    console.log('oldVal:', oldVal)
                },
                deep: true.immediate: true}},],methods: {
        fun (newVal, oldVal) { 
            console.log('newVal:', newVal)
            console.log('oldVal:', oldVal)
        }
    }
})
Copy the code

vm.$watch API

vm.$watch( expOrFn, callback, [options] )

  • Parameters:

  • {string | Function} expOrFn

  • {Function | Object} callback

  • {Object} [options]

    {Boolean} deep To detect changes in an object’s internal values, you can specify deep: true in the option argument. Note that listening for array changes does not need to do this

    {Boolean} immediate Specifying immediate: true in the option argument triggers the callback immediately with the current value of the expression

  • Return: {Function} unwatch

Source code analysis

Vue instantiation entry

In vue/ SRC /core/index.js, you can see import vue from ‘./instance/index’, importing vue.

In vue/SRC/core/instance/index, js,

import { initMixin } from './init'
import { stateMixin } from './state'
/ /...

function Vue (options) {
  / /...
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
// ...

export default Vue
Copy the code

As you can see, Vue is a function method that calls an initialization method called _init and passes in the options argument. The file also executes the initMixin and stateMixin methods.

InitMixin and _init

In the vue/SRC/core/instance/init. Js,

// ...
import { initState } from './state'
import { extend, mergeOptions, formatComponentName } from '.. /util/index'

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options? :Object) {
    const vm: Component = this

    // ...

    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // ...
    initState(vm)

    // ...}}Copy the code

See that the _init method is defined in the initMixin method. In the _init method, the constant VM is declared and the current instance is assigned, the options are accepted and processed, and the initState method is called.

initState

In the vue/SRC/core/instance/state. Js,

import {
  set,
  del,
  observe,
  defineReactive,
  toggleObserving
} from '.. /observer/index'

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

vue/src/core/util/env.js

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

If watch exists and the native watch method of firefox Object is excluded, the initWatch method is called.

initWatch

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 calls createWatcher for each property of the watch object passed in, passing in the current instance, property name, and property value.

Remember this from the beginning:

/ /...
    text4: [
        'fun'.function(newVal, oldVal){
            console.log('newVal:', newVal)
            console.log('oldVal:', oldVal)
        },
        {
            handler: function(newVal, oldVal){
                console.log('newVal:', newVal)
                console.log('oldVal:', oldVal)
            },
            deep: true.immediate: true,},]/ /...
Copy the code

So it makes sense that if the property value is an array, we iterate over it, calling createWatcher multiple times with different handlers using the same key.

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

vue/src/shared/util.js

export function isPlainObject (obj: any) :boolean {
  return _toString.call(obj) === '[object Object]'
}
Copy the code

Remember this from the beginning:

/ /...
    text1: 'fun'.'text3.val': {
        handler: function(newVal, oldVal){
            console.log('newVal:', newVal)
            console.log('oldVal:', oldVal)
        },
        deep: true.immediate: true,}/ /...
Copy the code

If handler is an Object instance, take the value of the handler property of the handler Object as the second argument to vm.$watch and the handler Object as the third argument.

If handler is a string, the property is retrieved from the instance object as the second argument.

stateMixin

export function stateMixin (Vue: Class<Component>) {
  / /...

  Vue.prototype.$watch = function (
    expOrFn: string | Function, cb: any, options? :Object
  ) :Function {
    const vm: Component = this
    // If the second argument is still an object, go back to createWatcher
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // Instantiate the Watcher observer instance
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // If immediate is set, call a callback directly
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}// Returns the unwatchFn method, which can pseudo-delete the current observer instance through a closure
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
Copy the code

The Watcher constructor

vue/src/core/observer/watcher.js

Recall that in the vUE responsive principle of source code analysis (see this if you can’t open it), our data object has an __ob__ attribute corresponding to an Observer instance. The Observer instance overwrites each attribute on data and holds the respective DEP array for each attribute through a closure. Each DEP array collects all Watcher instances of this property, and each observer instance has a set of DEPS dependencies that reverse collect the closure’s DEP.

So with that in mind, let’s take a look at Watcher a little bit

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
    }
    // _watcher holds the observer instance
    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 // Note that the id of the Watcher instance is incremental
    this.active = true
    this.dirty = this.lazy
    this.deps = [] // The added dependency array
    this.newDeps = [] // A cache array to hold dependencies to be added
    this.depIds = new Set(a)// Add an array of dependent ids
    this.newDepIds = new Set(a)// A cache array to hold the dependency ids to be added
    this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    // expOrFn may be a function or a string representation of attributes on an object, such as "a.b", which is parsed by parsePath and returned as a function
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      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
        )
      }
    }
    // Lazy is false to perform get initialization
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  // Get the value of the listener attribute to collect DEP dependencies
  get () {
    // Change dep. target to point to the current Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // Execute the getter to get the listening data property and fire the deP-dependent depend() method corresponding to the property to call addDep
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      // If depth listening is set
      if (this.deep) {
        traverse(value) // Call traverse to recursively traverse arrays and object types, firing each getter
      }
      // Change dep. target to null
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  // Add dependencies, called in dep dependent depend(),
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) { 
      // Add the dependency to the cache
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      // Call the DEP-dependent addSub to collect the current observer
      if (!this.depIds.has(id)) {
        dep.addSub(this)}}}// Clear cache dependencies
  cleanupDeps () {
    // Iterate over the dependent array
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      // DePs that are not in the cache need to call deP-dependent removeSub to remove the current observer
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)}}// Set newDepIds and newDeps to depIds and deps and clear the cache
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  / / update
  update () {
    // Set lazy to true and dirty to true
    if (this.lazy) {
      this.dirty = true
    } 
    // To synchronize, run is executed
    else if (this.sync) {
      this.run()
    }
    QueueWatcher is pushed asynchronously to the observer queue, which is eventually called to the run method via nextTick
    else {
      queueWatcher(this)}}// Update the value and perform the callback
  run () {
    // active Defaults to true
    if (this.active) {
      const value = this.get()
      // If the value is unequal, or if the value is an array or object, or if it is deep listening
      if( value ! = =this.value ||
        isObject(value) ||
        this.deep
      ) {
        // Assign the latest value to this.value
        const oldValue = this.value
        this.value = value
        // Execute Watcher's callback
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  // Trigger GET to set dirty to false
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  // Iterate through deps to execute each deP dependent depend method
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  // Delete the current Watcher instance
  teardown () {
    if (this.active) {
      Removes itself from the array of observer instances of the current Vue instance when _isBeingDestroyed is false
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)}// Execute removeSub for each DEP dependency to remove the current watch
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)}// Set active to false
      this.active = false}}}Copy the code

ExpOrFn: expOrFn: audit, audit, audit, audit, audit, audit, audit, audit

/ / key path
vm.$watch('a.b.c'.function (newVal, oldVal) {
  // Do something about it
})

/ / function
vm.$watch(
  function () {
    // the expression 'this.a + this.b' yields a different result each time
    // The handler function is called.
    // This is like listening on an undefined computed property
    return this.a + this.b
  },
  function (newVal, oldVal) {
    // Do something about it})Copy the code

vue/src/core/util/lang.js

const bailRE = new RegExp(` [^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string) :any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('. ')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}
Copy the code

Vue/SRC/core/observer/traverse by js:

const seenObjects = new Set(a)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
  }
  // Avoid repeated traversal
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  // Deeply iterate over groups and objects
  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

queueWatcher

Vue/SRC/core/observer/scheduler js:

const queue: Array<Watcher> = [] // Observer queue
let has: { [key: number]: ?true } = {} // The object used to hold the observer ID
let waiting = false // It is used to determine whether the last round of nextTick's observer clearing task is complete
let flushing = false // To determine whether the observer queue is being emptied
let index = 0 // The index of the observer in the queue that is emptying

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    If the queue is not emptying, push the new observer to the end of the queue
    if(! flushing) { queue.push(watcher) }// If the queue is being emptied
    else {
      let i = queue.length - 1
      /** The observer being emptied is not the last in the queue, and the last observer ID is greater than the id passed in, * (note that in the Watcher constructor section we know that the id of the observer instance is an increasing number, so we can make the above comparison) * Then we need to insert the observer into the middle of the queue. * If you are emptying the last observer, the effect is the same as if above, insert the end of the queue, and the next round nextTick emptying. * /
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1.0, watcher)
    }
    // The last round has been cleared
    if(! waiting) { waiting =true
      // Set synchronization is called directly
      if(process.env.NODE_ENV ! = ='production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      Async passes the cleanup method to nextTick
      nextTick(flushSchedulerQueue)
    }
  }
}
Copy the code

FlushSchedulerQueue Flushes the observer queue

export const MAX_UPDATE_COUNT = 100
const activatedChildren: Array<Component> = []

// Reset the state
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if(process.env.NODE_ENV ! = ='production') {
    circular = {}
  }
  waiting = flushing = false
}
// Perform queue emptying
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  /** Sort the observer queue before empting it to make sure: * 1. The parent component updates before the child component because the parent component was created before the child component * 2. User Watchers updates before Render Watcher * 3. If a parent component's Watcher is emptying and a child component is destroyed, the child component's watcher skips */
  queue.sort((a, b) = > a.id - b.id)

  // Since the queue can still change during the emptyprocess, the queue length is dynamically fetched each round
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    // Clear the current watcher id from has here
    has[id] = null
    // Execute watcher's run method to execute the callback
    watcher.run()
    // in dev build, check and stop circular updates.
    // Non-generated environment, if there is a watcher ID in has
    if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
      // Record the number of times the watcher has been emptied
      circular[id] = (circular[id] || 0) + 1
      // If you exceed the set maximum limit of 100 times, alert to the possibility of an infinite loop
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break}}}// Save the copy before resetting the state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // Invokes the component's updated/activated hook
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')}}Copy the code

I won’t expand on the life cycle