preface

Watch is a user-defined data listener. When the listening attribute changes, the callback will be triggered. This configuration is very common in services. It is also a must-ask in interviews, and is used for comparison with computed data.

So this article will take you to understand the workflow of Watch from the source code, as well as the implementation of dependency collection and deep listening. Before I do, I hope you have some understanding of the reactive principle flow, dependency collection flow, so that it will be easier to understand.

Previous articles:

Touch your hand to understand the Vue responsive principle

Hand touching takes you to understand the Computed principles of Vue

Watch the usage

“Know yourself and know your opponent, to win every battle”, before analyzing the source code, first know how it is used. This has a certain auxiliary effect for the later understanding.

First, string declarations:

var vm = new Vue({
  el: '#example'.data: {
    message: 'Hello'
  },
  watch: {
    message: 'handler'
  },
  methods: {
    handler (newVal, oldVal) { / *... * /}}})Copy the code

Second, function declarations:

var vm = new Vue({
  el: '#example'.data: {
    message: 'Hello'
  },
  watch: {
    message: function (newVal, oldVal) { / *... * /}}})Copy the code

Third, object declaration:

var vm = new Vue({
  el: '#example'.data: {
    peopel: {
      name: 'jojo'.age: 15}},watch: {
    // Fields can use the dot operator to listen for a property of an object
    'people.name': {
      handler: function (newVal, oldVal) { / *... * /}}}})Copy the code
watch: {
  people: {
    handler: function (newVal, oldVal) { / *... * / },
    // The callback will be invoked immediately after the listening starts
    immediate: true.// The object depth listens for any property change in the object to trigger a callback
    deep: true}}Copy the code

Fourth, array declarations:

var vm = new Vue({
  el: '#example'.data: {
    peopel: {
      name: 'jojo'.age: 15}},// Pass in an array of callbacks, which are called one by one
  watch: {
    'people.name': [
      'handle'.function handle2 (newVal, oldVal) { / *... * / },
      {
        handler: function handle3 (newVal, oldVal) { / *... * /}}]},methods: {
    handler (newVal, oldVal) { / *... * /}}})Copy the code

The working process

Entry file:

/ / source location: / SRC/core/instance/index, js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '.. /util/index'

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

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
Copy the code

_init:

/ / source location: / SRC/core/instance/init. Js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options? : Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    // 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 {
      // mergeOptions merges the mixin option with the options passed in by new Vue
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // Initialize the data
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
Copy the code

initState:

/ / source location: / SRC/core/instance/state. Js
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 */)}if (opts.computed) initComputed(vm, opts.computed)
  // This initializes watch
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

initWatch:

/ / source location: / SRC/core/instance/state. Js
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      / / 1
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      / / 2
      createWatcher(vm, key, handler)
    }
  }
}
Copy the code
  1. Array declaredwatchThere are multiple callbacks that need to be created in a loop
  2. Other declarations are created directly

createWatcher:

/ / source location: / SRC/core/instance/state. Js
function createWatcher (vm: Component, expOrFn: string | Function, handler: any, options? : Object) {
  / / 1
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  / / 2
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  / / 3
  return vm.$watch(expOrFn, handler, options)
}
Copy the code
  1. Object declaredwatchRetrieves the corresponding callback from the object
  2. string-declaredwatch, directly take the method on the instance (note:methodsCan be obtained directly on the instance.
  3. expOrFnwatchkeyValue,$watchUse to create a userWatcher

So, in addition to the watch configuration, you can also call the instance’s $watch method to achieve the same effect when creating a data listener.

$watch:

/ / source location: / SRC/core/instance/state. Js
export function stateMixin (Vue: Class<Component>) {
  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)
    }
    / / 1
    options = options || {}
    options.user = true
    / / 2
    const watcher = new Watcher(vm, expOrFn, cb, options)
    / / 3
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}/ / 4
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
Copy the code

StateMixin is already called in the entry file, adding the $watch method to the Vue prototype.

  1. All UsersWatcheroptions, will carryuserlogo
  2. createwatcher, for dependency collection
  3. immediateWhen true, the callback is invoked immediately
  4. The returned function can be used to cancelwatchListening to the

Rely on the collection and update process

After going through the above process, you will eventually enter the logic of New Watcher, which is also the trigger point for dependency collection and updates. Now let’s see what happens in this.

Depend on the collection

/ / source location: / SRC/core/observer/watcher. Js
export default class Watcher {
  constructor( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) {this.vm = vm
    // 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)// parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}
Copy the code

Inside the Watcher constructor, the callbacks and options passed in are saved, which is beside the point. Let’s focus on this code:

if (typeof expOrFn === 'function') {
  this.getter = expOrFn
} else {
  this.getter = parsePath(expOrFn)
}
Copy the code

The exported FN is the key value of watch, because the key value may be obJ.A.B, parsePath is needed to parse the key value, which is also the key step of dependent collection. What it returns is a function, so let’s not worry about what parsePath is doing, but let’s move on.

The next step is to call get:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    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

PushTarget attaches the current “user Watcher” (i.e. current instance this) to dep. target, which is used to collect dependencies. And then you call the getter function, and that’s where you get parsePath’s logic.

// 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

The obj parameter is a VM instance, and segments is a parsed array of key values that loops over the value of each key, triggering a data-jacking GET. Dep. Depend is then triggered to collect the dependency (the dependency is the Watcher hanging on dep. target).

At this point, dependency collection is complete, and we know from the above that dependency collection is triggered for each key value, meaning that a change in the value of any of the above keys triggers the Watch callback. Such as:

watch: {
    'obj.a.b.c': function(){}}Copy the code

Not only do changes to C trigger callbacks, but also changes to B, A, and obj trigger callbacks. The design is also clever, with a simple loop to collect dependencies for each item.

update

The first thing that triggers an update is a “data hijacking set”, which calls dep.notify to notify each watcher update method.

update () {
  if (this.lazy) {dirty is set totrue
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)}}Copy the code

Then we go to queueWatcher for asynchronous updates, not asynchronous updates. All you need to know is that the last thing it calls is the run method.

run () {
  if (this.active) {
    const value = this.get()
    if( value ! = =this.value ||
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      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)
      }
    }
  }
}
Copy the code

Get gets the new value, call this.cb, and pass in the new value and the old value.

The depth of the listening

Depth monitoring is a very important configuration of watch monitoring, which can observe the change of any attribute in the object.

Back to the get function, there is a code that looks like this:

if (this.deep) {
  traverse(value)
}
Copy the code

To determine if you need a deep listen, call traverse and pass in the values

/ / source location: / 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
  }
  if (val.__ob__) {
    / / 1
    const depId = val.__ob__.dep.id
    / / 2
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  / / 3
  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
  1. depIdIs a unique identifier for each property being observed
  2. Deduplication prevents the same attribute from executing logic repeatedly
  3. Using different strategies for arrays and objects, the ultimate goal is to recursively acquire each property, triggering their “data hijacking.get“Collect dependencies, andparsePathThe effect is similar

It can be concluded from this that deep monitoring using recursion to monitor, there will certainly be performance loss. Because each attribute has to go through the dependency collection process, avoid this in your business.

Uninstall listening

This is one of those rare but useful ways of doing business that is rarely used and not a priority. As a part of Watch, its principle is also explained here.

use

Here’s how it’s used:

data(){
  return {
    name: 'jojo'
  }
}
mounted() {
  let unwatchFn = this.$watch('name', () => {})
  setTimeout((a)= >{
    unwatchFn()
  }, 10000)}Copy the code

When you use $watch to listen for data, a corresponding unload listener is returned. As the name suggests, calling it, of course, stops listening for data.

The principle of

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) {
    try {
      // Call watch immediately
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}return function unwatchFn () {
    watcher.teardown()
  }
}
Copy the code

You can see that the returned unwatchFn actually performs teardown.

teardown () {
  if (this.active) {
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this)}let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)}this.active = false}}Copy the code

The action in teardown is also very simple, iterating through deps and calling the removeSub method to remove the current Watcher instance. Watcher will not be notified of the next property update. Deps stores the DEP of an attribute.

Strange place

While looking at the source code, I noticed something strange about Watch, which led to its usage being like this:

watch:{
  name: {handler: {
      handler: {
        handler: {
          handler: {
            handler: {
              handler: {
                handler: (a)= >{console.log(123)},
                immediate: true
              }
            }
          }
        }
      }
    }
  }
}
Copy the code

Normally, a handler passes a function as a callback, but for object types, the internal recursive fetch is performed until the value is a function. So you can send unlimited doll objects.

The recursive point in $watch is this code:

if (isPlainObject(cb)) {
  return createWatcher(vm, expOrFn, cb, options)
}
Copy the code

If you know the actual application of this code, please let me know

conclusion

The watch listening implementation uses traversal to obtain properties and triggers “data hijacking GET” to collect dependencies one by one. The advantage of this is that the callback can be executed even when the properties of its superior are modified.

Unlike Data and computed, which collect dependencies during page rendering, Watch collects dependencies before page rendering.

In an interview, if we are asked about the differences between computed and watch, here are some answers:

  • One is thatcomputedWant to rely ondataReturns a value for the property change on,watchObserving data triggers a callback;
  • The second iscomputedwatchDependency collection occurs at different points;
  • The third iscomputedThe update requires “renderingWatcher“With the help of,watchNo, I mentioned this in my last article.