directory

Vm.$watch Internal principle 4. Implementation principle of the deep parameter 5. Initialize watch 6. Summary

1. Service scenarios

Watch is used to monitor the change of some data, and then call what function to process it. A single piece of data can affect multiple pieces of data, such as browser adaptation, monitoring routing objects, monitoring its own property changes, and so on.

2. Usage

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

Definition: Observe a change in the evaluation result of an expression or function on a Vue instance. The callback takes new and old values. The expression accepts only supervised key paths. For more complex expressions, replace them with a function. Example:

// key path VM.$watch('a.b.c'.function(newVal, oldVal) {// do something}) // function VM$watch(
 function() {// The expression 'this.a + this.b' is called each time it yields a different result. // This is like listening on an undefined computed propertyreturn this.a + this.b
 },
 function(newVal, oldVal) {// do something})Copy the code

Option deep: To detect changes in the internal value of an object, you can specify deep: true in the option argument. Note that you don’t need to do this to listen for changes in the array.

vm.$watch('someObject', callback, {
  deep: true
})
vm.someObject.nestedValue = 123
// callback is fired
Copy the code

Option immediate: Specifying immediate: true in the option argument triggers the callback immediately with the current value of the expression.

vm.$watch('a', callback, {
  immediate: true}) // Trigger the callback immediately with the current value of 'a'Copy the code

Return value: vm.$watch returns a function that cancels this listening.

var unwatch = vm.$watch(
  'value'.function () {
    doSomething()
    if (unwatch) {
      unwatch()
    }
  },
  { immediate: true})Copy the code

3. Vm.$watch internal principle

Vue change detection vUE change detection vUE change detection vUE change detection vUE change detection

The vm.$watch parameter “deep” and “immediate” is not available. Let’s see how vm.$watch is implemented:

Vue.prototype.$watch=function(exp,cb,options){
  const vm=this
  options=options||{}
  const watcher=new Watcher(vm,exp,cb,options)
  if(options.immediate){
    cb.call(vm,watcher.value)
  }
  return function unwatchFn(){
    watcher.teardown()
  }
}
Copy the code

The logic is simple, execute new Watcher to implement the basic function of vm.$watch. But there is one detail: exp accepts functions, so the Watcher constructor needs to be changed.

exportDefault class watcher{constructor(vm,exp,cb){this.vm=vmif(typeof exp==='function'){
      this.getter=exp
    }else{
      this.getter=parsePath(exp)
    }
    this.cb=cb
    this.value=this.get()
  }
  ......
}
Copy the code

When exp is a function, it not only returns data dynamically, but all data it reads is observed by Watcher. Wacther will be notified when any of them change.

Return value: After executing vm.$watch, return a function unwatchFn to cancel the observation. To cancel the watch, we need to know who Watcher itself subscribes to, which DEPs the Watcher instance collects. Loop through the DEPS it collects and remove its dependencies from the Dep.

Add the addDep method to Watcher to record which DEPs you subscribe to:

exportConstructor (vm,exp,cb){constructor(vm,exp,cb){this.vm=vm // add this.deps=[] / add this.depids =new Set()if(typeof exp==='function'){
      this.getter=exp
    }else{this.getter=parsePath(exp)} this.cb=cb this.value=this.get()} // Add addDep(dep){const id=dep.id // dep watcher generates correlationif(! This.depids.has [id]){this.depids.add (id) //watcher add dep this.depids.push (dep) //dep add watcher depids.addSub (this)}}...... }Copy the code

Code analysis: We use depIds to determine that the current Watcher has subscribed to Dep and will not re-subscribe. When Watcher reads the value, the collection dependency logic is triggered. Add (id) to record that Watcher has subscribed to the Dep, push(Dep) to record which DEPs watcher has subscribed to, and finally trigger dep.addSub(this) to add himself to the Dep.

With addDep added to Watcher, the logic for collecting dependencies in Dep also needs to change:

letUid = 0 / / newexport  default class Dep{
  constructor(){
    this.id=uid++  //新增
    this.subs=[]
  }
  depend() {if(window.target){this.addSub(window.target) // Discard window.target.adddep (this) // add}}}Copy the code

Code analysis: At this point, the Dep records which Watcher needs to be notified when the data changes, and the Watcher records which DEPS it has been notified by. So Dep and Watcher are many-to-many

Why this is many-to-many: We know that when a view uses the same data multiple times, one Dep corresponds to multiple Watchers. Also, when a watcher looks at a function that uses multiple data, the Watcher collects multiple DEPs.

Unwatcher: Now that we know which dePs Watcher subscribed to, we can add a teardown method in Watcher to notify those subscriptions and remove them from the dependency list:

// Remove yourself from the DEP listteardown() {let i=this.deps.length
  while(i--){
    this.deps[i].removeSub(this)
  }
}
Copy the code

Add removeSub method to deP:

removeSub(sub){
    const index=this.subs.indexOf(sub)
    if(index>-1){
      return this.subs.splice(index,1)
    }
  }
Copy the code

This is how Unwatch works. When data changes, watcher is not notified that it has been deleted.

4deep parameter principle implementation

Deep: deep: deep: deep: deep: deep: deep: deep: deep: deep

exportDefault class constructor {constructor(vm,exp,cb){this.vm=vm // addif(options){ this.deep=!! options.deep }else{
     this.deep=false
   }
    this.deps=[]
    this.depIds=new Set()
    if(typeof exp==='function'){
      this.getter=exp
    }else{
      this.getter=parsePath(exp)
    }
    this.cb=cb
    this.value=this.get()
  }
  get(){
    window.target=this
    letCall (value = this. Getter. This vm, enclosing the vm) / / newif(this.deep){
      traverse(value)
    }
    window.target=undefined
    return value
  }
}
Copy the code

If the user uses the deep argument, traverse will be called before window.target=undefined to handle the deep logic. Otherwise, Watcher would not collect the dependency list of subvalues.

Traverse function: This function is very simple. It’s a recursive way of recursing all the subvalues of value to trigger them to collect dependencies.

5. Initialize watch

Type: {[key: string] : string | Function | Object | Array}

An object whose key is the expression to observe and whose value is the corresponding callback function. The value can also be a method name, or an object that contains options. The Vue instance will call $watch() at instantiation time, iterating through each property of the Watch object.

Example:

var vm = new Vue({
  data: {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
    e: {
      f: {
        g: 5
      }
    }
  },
  watch: {
    a: function (val, oldVal) {
      console.log('new: %s, old: %s', oldVal, oldVal)},'someMethod'// This callback is called when the property of any object being listened on changes, no matter how deep it is nested c: {handler:function (val, oldVal) { /* ... */ },
      deep: true}, // This callback will be called immediately after listening starts d: {handler:'someMethod',
      immediate: true
    },
    e: [
      'handle1'.function handle2 (val, oldVal) { /* ... */ },
      {
        handler: function handle3 (val, oldVal) { /* ... */ },
        /* ... */
      }
    ],
    // watch vm.e.f's value: {g: 5} 'e.f': function (val, oldVal) { /* ... */ } } }) vm.a = 2 // => new: 2, old: 1Copy the code

$watch = vm.$watch = vm.$watch = vm.$watch = vm.

But the value of the Watch option supports strings, functions, arrays, and objects. Different types have different uses, so we need to do some adaptation when calling vm.$watch.

function initWatch(vm,watch){
  for(const key in watch){
    const handler=watch[key]
    if(Array.isArray(handler)){
      for(leti=0; i<handler.length; i++){ createWatcher(vm,key,handler[i]) } }else{
      createWatcher(vm,key,handler)
    }
  }
}
Copy the code

Code analysis: First divide watch option values into two classes, array and others. The createWatcher function is then called to handle the other types and vm.$watch is called to create the observation expression.


function createWatcher(vm,exp,handler,options){
  if(isPlainObject(handler)){
    options=handler
    handler=handler.handler
  }
  if(typeof handler==='string'){
    handler=vm[handler]
  }
  return vm.$watch(exp,handler,options)
}
Copy the code

Code analysis: Three types are handled:

  • Function: Pass directly to vm.$watch without special handling
  • String: Takes the method from the VM and assigns it to handler
  • The options value is set to handler and the variable handler is set to handler object handler method.

That’s it for watch initialization.

6. Summary

In fact, watch is not very difficult, mainly because Watcher class and Dep class are flexibly used.

One API at a time, one day at a time.