preface

This is the third one, and each one is a big one. And all three are highly correlated, so it is recommended to look at Data, computed, and watch, just as in the initialization process in the source code

Vue parse: data

Vue parse: computed

Again, let’s start with the simplest example

<div id="app">
  {{a}}
</div>
<script>
  let vm = new Vue({
    el: '#app'.data: {
      a: 1.b: 2.d: 3
    },
    watch: {
      a: function (val, oldval) {
        console.log('new: %s, old: %s', val, oldval)
      },
      // Object form
      b: {
        handler: function (val, oldval) {
          console.log('new: %s, old: %s', val, oldval)
        },
        deep: true
      },
      d: {
        handler: 'someMethod'.immediate: true
      },
      e: [
        function handle2() {},
        function handle3() {},
        function handle4() }, {},]methods: {
      someMethod(val, oldval) {
        console.log('new: %s, old: %s', val, oldval)
      }
    }
  })
  </script>
Copy the code

As you can see, watch can be written in many different ways, and there are many more in the official documentation API. Click to enter and view. There are so many forms, in the vUE processing will certainly not be processed individually, need to be unified into a format, convenient for later processing. That’s where the merge strategy comes in.

In the _init method, we have code that serializes user-written properties like props, data, methods, watch, computed, and so on into the format that vUE needs through the policy mode, so we just put a breakpoint here, Look at the vm.options generation format.

// Merge options and assign them to $options
vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  // The options passed in by the user may be empty
  options || {},
  vm
)
Copy the code

As you can see, it is still an object, corresponding to the three formats in which the rest of the code is parsed

{
  watch: {a: ƒ (val, oldval)
    b: {deep: true.handler: ƒ}
    d: {handler: 'someMethod'.immediate: true}
    e: (3[ƒ, ƒ, ƒ]}}Copy the code

initWatch

In the initState method, we can see that we are taking the vm.$options data, and there is a judgment opts.watch! = = nativeWatch. This is because in Firefox, Object has a watch method, so you need to make a judgment call.

// instance/state.js 
export function initState (vm: Component) {
  // This array will be used to store all the Watcher objects for this component instance
  vm._watchers = []
  const opts = vm.$options
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

And then we’re going to go inside initWatch, and we’re just going to take the key and the value and pass it to createWatcher, and we’re just going to do the different formats once. The createWatcher method handles handle of object type and handle of string type respectively. As you can see, the handle value of the string type is obtained from the VM, so you can guess that methods are defined on the instance as well as options.

Note:

Finally, vue calls vm.$watch, so whether it is functional watch or object form, $watch will be called at the end, which is the beginning of watch execution

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    // If it is an array, loop
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
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

vm.$watch

The first judgment is that when we use $watch to create a listener, we need to readjust cb. For example, cb can be of the form {handle:function(){}, deep:true}, in which case the options passed in are overwritten. $watch(‘e’, {handle:function(){}, deep:true}, {immediate: true}).

The next two lines of code are the core. Vue adds a user attribute to options and assigns it to true. New Watcher then creates the constructor. You can see that this is the third kind of Watcher, and we’ll call it user Watcher.

Vue.prototype.$watch = function (
  expOrFn: string | Function, cb: any, options? :Object
) :Function {
  // Current component instance object
  const vm: Component = this
  // Check if the second argument is pure
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  // Created for a user
  options.user = true
  // Create a Watcher object
  const watcher = new Watcher(vm, expOrFn, cb, options)
  ...
}
Copy the code

Ok, next, look at New Watcher, because this code has been posted several times, I will pick up what I haven’t talked about before, and wait for a separate chapter to talk about options. ExpOrFn or other expOrFn or other expOrFn or other url expressions, watcher supports strings, so this is likely to be expored using the parsePath method, which returns a expOrFn or other url processed function, and is able to export OBj, similar to the simple response that was written earlier, if obJ is passed this, So we call this[a], and the second time is this[a][b]

export default class Watcher {
  constructor (
    vm: Component,
    // Evaluate the expression
    expOrFn: string | Function./ / callback
    cb: Function./ / options
    options?: ?Object.// Whether to render watcherisRenderWatcher? : boolean) {
    // options.this.cb = cb / / callback
    this.id = ++uid // uid for batching
    this.active = true // Activate the object
  
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // process the expression obj.a
      this.getter = parsePath(expOrFn)
    }
    // Attribute constructors are evaluated without value
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}
// core/util/lang.js
export function parsePath (path: string) :any {
  const segments = path.split('. ')
  Obj [a][b]
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}
Copy the code

A normal watch with no options passed in will normally execute the this.get method. The pushTarget method, which has been covered several times, has only two functions

  1. willDep.targetAssign the current valueWatcher
  2. The currentWatcherIn thetargetStackIn the array

Then look at this code this.getter.call(vm, vm), and see that the return from parsePath assigns a value to this.getter, so we’re actually executing the function that parsePath returns, and we’re passing in the VM, which is exactly what I said above. A, this._data. A triggers the interceptor for A in data.

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    ...
  } finally {
    // Clear the current target
    popTarget()
    // Clear dependencies
    this.cleanupDeps()
  }
  return value
}
Copy the code

As with computed, the current user watcher is stored in the dep.subs corresponding to A. The process is not detailed, it is recommended to debug yourself. Return to the get method and follow the cleanup process. Does that end the initialization? No, we’re going to go back to the $watch method. Forget the intermediate steps, we just implemented New Watcher. We then go through the following process and return an unwatchFn. This method, you can do teardown

Vue.prototype.$watch = function (
    expOrFn: string | Function, cb: any, options? :Object
  ) :Function {...const watcher = new Watcher(vm, expOrFn, cb, options)
    ...
    // Return a cancel function
    return function unwatchFn () {
      watcher.teardown()
    }
  }

Copy the code

So this initialization is done. Let’s start with the example.

Trigger a watch

So let’s change the example, let’s test it with the simplest example.

<div id="app">
</div>
<script>
 let vm = new Vue({
   el: '#app'.data: {
     a: 1,},watch: {
     a: function (val, oldval) {
       console.log('new: %s, old: %s', val, oldval)
     },
   }
 })
</script>
Copy the code

Here we do something different by removing the {{a}} from template and looking at the anonymous function generated by vm._render

(function anonymous(
) {
with(this){return _c('div', {attrs: {"id":"app"}})}})Copy the code

Without a, then a’s get is not called. This will not collect a’s rendering watcher. So there’s only one user watcher on A. And then we trigger the set of A. On the console, run vm.a = 6. Update –>queueWatcher(this)–>nextTick(flushSchedulerQueue) nextTick(flushSchedulerQueue) The current user Watcher is put into a queue that is executed in the flushSchedulerQueue.

In flushSchedulerQueue, the watcher is removed from the queue and executed sequentially. In this case, the watcher.

run () {
 // Whether the observer is active
 if (this.active) {
   // reevaluate
   const value = this.get()
   // This is never thrown in the rendering function because both values are undefiend
   if( value ! = =this.value ||
     // If the value is equal, it may be an object reference. If the value is changed, the reference is still the same.
     // Execute if yes
     isObject(value) ||
     this.deep
   ) {
     // Save the old value and set the new value
     const oldValue = this.value
     this.value = value
     // The observer is defined by the developer as watch $watch
     if (this.user) {
       const info = `callback for watcher "The ${this.expression}"`
       invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
     } else {
       this.cb.call(this.vm, value, oldValue)
     }
   }
 }
}
Copy the code

This method will be discussed in detail. First, an evaluation is performed, mainly to get the new value, and the subsequent dependencies will be skipped by the repeated judgment because they already exist. Both the old and new values are cached, and now we have user=true, so invokeWithErrorHandling is done. The method is to execute the handle we define, but since it’s user-defined, we need a try catch. This completes the entire Watcher implementation.

export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}
Copy the code

Options How to execute parameters in vUE

Now that the basic watch is parsed, let’s take a look at the execution of each option in vUE. So this is the code inside the constructor when I was new Watcher

if (options) {
  this.deep = !! options.deep// Whether to use depth observation
  this.user = !! options.user// Identifies whether the current observer instance object is developer-defined or internally defined
  this.lazy = !! options.lazy// Lazy Watcher does not request the first time
  this.sync = !! options.sync// Whether to evaluate and perform callbacks synchronously when data changes
  this.before = options.before // Call the callback before triggering the update
}
Copy the code

immediate

Again using the original example, we add options.

<div id="app">
</div>
<script>
 let vm = new Vue({
   el: '#app'.data: {
     a: 1,},watch: {
     a: {
       handler: function (val, oldval) {
         console.log('new: %s, old: %s', val, oldval)
       },
       immediate: true}}})</script>
Copy the code

Then look at the source code. It’s very simple. During initialization, the invokeWithErrorHandling is performed immediately after the new Watcher is finished, that is, the custom function callback is performed, and the value passed in is the value that the current new Watcher computed.

Vue.prototype.$watch = function (
 expOrFn: string | Function, cb: any, options? :Object
) :Function {
 // Execute immediately
 if (options.immediate) {
   const info = `callback for immediate watcher "${watcher.expression}"`
   pushTarget()
   // Get the observer instance object, this.get
   invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
   popTarget()
 }
 
}
Copy the code

lazy

The essence of computed data is lazy watcher. And VUE implements value caching for us. So normally we don’t pass lazy in watch

sync

Set this value, as the name indicates, to sync. Look at this code. In the Watcher class update method, when we trigger the interceptor set, we loop through dep.notify to execute Watcher’s update method. Instead of putting the current Watcher on the microtask queue, it executes it directly.

update () {
 /* istanbul ignore else */
 // The value of the calculated attribute does not participate in the update
 if (this.lazy) {
   this.dirty = true
   // Whether to synchronize changes
 } else if (this.sync) {
   this.run()
 } else {
   // Put the current observer object into an asynchronous update queue
   queueWatcher(this)}}Copy the code

deep

Modify the example

<div id="app">
</div>
<script>
 let vm = new Vue({
   el: '#app'.data: {
     b: {
       c: 2.d: 3}},watch: {
     b: {
       handler: function(val, oldval) {
         console.log(`new: The ${JSON.stringify(val)}, old: The ${JSON.stringify(oldval)}`)},deep: true}}})</script>
Copy the code

Deep stands for deep listening, so where does vUE trigger the interceptor for deep objects? After a gets triggered by the get interceptor and stores watcher, it should be obvious. Look at the Get method in the Watcher class, where the evaluation expression is called

get () {
 // Assign the dep. target value Watcher
 pushTarget(this)
 let value
 const vm = this.vm
 try {
   value = this.getter.call(vm, vm)
 } catch (e) {
   ...
 } finally {
   if (this.deep) {
     traverse(value)
   }
   // Clear the current target
   popTarget()
   // Clear dependencies
   this.cleanupDeps()
 }
 return value
}
Copy the code

Before clearing the dependency, vue evaluates the deep and then calls the traverse method.

The code here is a little harder to understand, we start from the beginning, first firing b’s get when the evaluation expression is triggered for the first time, and then putting the user Watcher into the deP closure defined by defineReactive on B. We indicate that there is also an __ob__ created by the New Observer

{b(--> closure dep{subs:[Watcher], id:3}) : {c: 2.d: 3.__ob__: {
      value: {},
      id: 4.subs: []}}__ob__: {
    value: {},
    id: 2.subs: []}}Copy the code

Here’s a reminder of the initialization of the data nested object, and looking at the source code again, childOb has a value, it’s held by the closure after initialization, and the value is the object of B, and value is it. Since it has a value, we go to childob.dep.depend (), at which point we save a watcher in __ob__.

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  / / rely on the box
  const dep = new Dep()
  letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
    enumerable: true.get: function reactiveGetter () {
      // Perform custom getters if custom getters exist
      const value = getter ? getter.call(obj) : val
      // Dependencies to be collected
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
  })
}
Copy the code

In other words, watcher’s newDepIds have two values [3, 4].

{b(--> closure dep{subs:[Watcher], id:3}) : {c: 2.d: 3.__ob__: {
      value: {},
      dep: {
        id: 4.subs: [
         Watcher // Save the user watcher through childOb]},vmCount:0}}__ob__: {
    value: {},
    dep: {
      id: 2.subs: []},vmCount: 1}}Copy the code

Then b’s interceptor ends, and the traverse method comes in. First look at the value of val, which we got from the last calculation, that is, the value of b is the object.

const seenObjects = new Set(a)export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  // Check if val is an array
  // * val is the value of the observed attribute
  const isA = Array.isArray(val)
  // * Resolve the issue of circular references causing an infinite loop
  // Get the unique value in the Dep for reactive object removal
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  Val [I] and val[key[I]] are both evaluated, which triggers the purple property's GET interceptor
  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

The middle part of the __ob__ judgment is not mentioned, the comment is very clear, in fact, when we have cross-reference, if there is __ob__, exit. Avoid the loop. If val[keys[I]] triggers the interceptor of D, the watcher will be added to the DEP of D, as well as to the DEP of C.

{c(--> closure dep{subs:[watcher], id:5}) :2, d(--> closure dep{subs:[watcher], id:6}) :3.__ob__: {
   value: {},
   dep: {
     id: 4.subs: [
      Watcher // Save the user watcher through childOb]},vmCount:0}}Copy the code

Since the DEP saves the user Watcher on all related attributes, we can set the watcher to trigger multiple attributes. Try the following code

vm.b.c = 7
// new: {"c":7,"d":3}, old: {"c":7,"d":3}
vm.b.d = 8
// new: {"c":7,"d":8}, old: {"c":7,"d":8}
vm.b = 6
// new: 6, old: {"c":7,"d":8}
vm.$set(vm.b, 'e'.6)
// new: {"c":2,"d":3,"e":6}, old: {"c":2,"d":3,"e":6}
Copy the code

Since vue dep holds the user watcher in the __ob__ attribute on the b object, the operation on B is also valid, unless we really want to do so, which is actually quite costly in depth observation, if the level is a little higher. We have a better way of doing this.

Looking at the parsePath method, there’s this code path.split(‘.’), so if we want to look deep, for example if we want to look at C, we can write this

<div id="app">
</div>
<script>
 let vm = new Vue({
   el: '#app'.data: {
     b: {
       c: 2.d: 3}},watch: {
     'b.c': {
       handler: function(val, oldval) {
         console.log('new: %s, old: %s', val, oldval)
       },
     }
   }
 })
</script>
Copy the code

So we save watcher only for b, __ob__, and c. If YOU have a lot of properties in B, that’s n minus 1 over n. That’s a big optimization.

before

This is not a property used in official documentation, but it can be used, as shown below

<div id="app">
</div>
<script>
 let vm = new Vue({
   el: '#app'.data: {
     a: 1
   },
   watch: {
     a: {
       handler: function (val, oldval) {
         // console.log(`new: ${JSON.stringify(val)}, old: ${JSON.stringify(oldval)}`)
         console.log('new: %s, old: %s', val, oldval)
       },
       before: function () {
         console.log('Called before')}}}})</script>
Copy the code

In the source code, it runs before watcher.run(), but when we use render watcher, it is used to trigger beforeUpdate. The above example will obviously run before the handler as well

if (watcher.before) {
   watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
Copy the code

Form of a function call

Using $watch is no different, but it does things that declarative doesn’t do. Think computed, it evaluates expressions all the time as functions in New Watcher. So obviously watch should also support passed in functions, which is what $watch does. Take the following example

<div id="app">
</div>
<script>
 let vm = new Vue({
   el: '#app'.data: {
     a: 1.b: 2
   },
   mounted() {
     this.$watch(() = > ([this.a, this.b]), (val, oldval) = > {
       console.log(`new: ${val}, old: ${oldval}`)})}})</script>
Copy the code

Either a or B triggers the listening callback, respectively. As for the principle obviously is a and B deP save the user watcher.

vm.a = 7
// 24 new: 7,2, old: 1,2
vm.b = 5
// new: 7,5, old: 7,2
Copy the code

teardown

One thing left out is that when we finish executing $watch, we return an unwatchFn. Such as example

<div id="app">
</div>
<script>
 let vm = new Vue({
   el: '#app'.data: {
     a: 1,},mounted() {
     let unwatch = this.$watch('a'.(val, oldval) = > {
       console.log(`new: ${val}, old: ${oldval}`)
       unwatch()
     })
   }
 })
</script>
Copy the code
vm.a = 5
// new: 5, old: 1
vm.a = 6
Copy the code

Executing it will find that the second set will not be executed, so all it does is clean up. Divided into three steps

  1. Remove the current_watchersIn the correspondingwatcher
  2. removedepThe current user insidewatcher, pay attention toDepInstance andWatcherIt’s mutual preservation, and this is for elimination
  3. Deactivate the observer
teardown () {
 if (this.active) {
   // Remove the Watcher object if the component is not destroyed
   if (!this.vm._isBeingDestroyed) {
     remove(this.vm._watchers, this)}let i = this.deps.length
   // An observer can view multiple attributes at the same time, so remove all attributes observed by the observer
   while (i--) {
     this.deps[i].removeSub(this)}// Deactivate the observer
   this.active = false}}Copy the code

Ending and mumbling

In this way, the analysis of Watch is finished. It is very helpful for me to write the articles in these days. The basic relevant codes have been debugged line by line. If someone has this idea, it is recommended to clean the cache in traceless mode and refresh f5 several times.

Added: Added teardown method resolution