Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

Preliminary preparation and materials

  • This article is based on vue2.6.14
  • Demo address portal (in order to facilitate the execution of the source code, please debug under the debugger view, in the demo key code has hit the Debugger)
  • Demo code
  • There are partial public methods of encapsulation in source code interpretation, such as:isPlainObject()Please check the source code

Interpretation of the

Front-end related code

<div id="app">
    <input type="text" v-model="name">
    <p>{{name}}</p>
    <button v-on:click="changeName">Change the name</button>
</div>
<script>
    let vue = new Vue({
        el: '#app'.data: {
            name: 'Joe'
        },
        watch: {
            name: function (val, oldVal) {
                console.log(`newVal:${val}; oldVal:${oldVal}`); }},methods: {changeName(){
                this.name = this.name === 'Joe' ? 'bill' : 'Joe'}}});</script>
Copy the code

The initState() method in vue is called when the page is initialized, and the methods related to initialization props, methods, data, computed, and wantch are called in initState, respectively. The code looks like this:

  function initState (vm) {
    vm._watchers = [];
    var 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); }
    // Call the initWatch method here to initialize watch
    debugger
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }
Copy the code

Now let’s look at what’s going on in the initWatch() method,initWatchThe two parameters of VM and Watch (the contents of watch during initialization) are received, and the data to be monitored in watch is traversed here to determine whether the value in Watch is an array (it can be seen that the use of watch can be defined as a function or an arrayportal), and called separatelycreateWatcherMethod to create the corresponding observer. The code is as follows:

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

In the createWatcher method, determine the hander in watch (referring to the name in the watch object)function (val, oldVal) {}If it is an object, set the options parameter to handler and recurse to handler. If Hangdler is just a string, it assigns the corresponding value in the VM instance (possibly the method name in the corresponding method) to the handler. Finally, execute the $watch method in the VM. The code is as follows:

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

Define the $watch method in the prototype of vue, and interpret the code in the comments (New Watcher, pushTarget, invokeWithErrorHandling, popTarget, watcher.teardown follow) as follows:

Vue.prototype.$watch = function (expOrFn,cb,options) {
      debugger
      var vm = this;
      // Determine if the cb callback is of an object type, and if so continue to call createWatcher
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
      }
      / / the options value assignment
      options = options || {};
      options.user = true;
      // Call new Watcher to create the corresponding observer, collect dependencies, listen for data changes, and do the corresponding processing
      var watcher = new Watcher(vm, expOrFn, cb, options);
      // Checks whether there is an immediate attribute. If there is an immediate attribute, the cb is executed immediately
      if (options.immediate) {
        var info = "callback for immediate watcher \"" + (watcher.expression) + "\" ";
        pushTarget();
        invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
        popTarget();
      }
      // Return unwatchFn to unwatch data, removing the watcher instance from the dependency list of the currently being watched state
      return function unwatchFn () { watcher.teardown(); }};Copy the code

Create Watcher instance to implement the monitoring and processing of data transformation. In addition, the Watcher prototype also defines get to evaluate getters and recollect dependencies, addDep adds dependencies to the directive, cleanupDeps cleans dependencies, Update is called when dependencies change, and Depend depends on all dePs collected by this observer, Teardown removes methods such as self from the list of all dependencies. The code is as follows:

  var Watcher = function Watcher (vm,expOrFn,cb,options,isRenderWatcher) {
    debugger
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !! options.deep;this.user = !! options.user;this.lazy = !! options.lazy;this.sync = !! options.sync;this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // If expOrFn is a function, assign it to the getter, otherwise use parsePath to read data in the property path, e.g., A.B.C
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn("The observer accepts only a simple point-separated path.", vm); }}// Call the get() method on the Watcher prototype for dependency collection
    this.value = this.lazy ? undefined : this.get();
  };
Copy the code

The Get method in the Watcher prototype does dependency collection as follows:

  Watcher.prototype.get = function get () {
    // Call pushTarget to evaluate whether the current Watcher has subscribed to the DEP, and collect dependencies if not
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\" "));
      } else {
        throw e
      }
    } finally {
      // Deep observation dependency
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  };
Copy the code

After we have gathered which DEPs we subscribed to in our dependencies, we can use the unwatchFn method in the $watch method code to remove self from the list of all dependencies by calling the Teardown method in the Watcher prototype. Here’s the code for teardown:

  Watcher.prototype.teardown = function teardown () {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this);
      }
      var i = this.deps.length;
      // Subscribe to the list loop and execute his removeSub method to remove himself from the list of dependencies
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false; }};Copy the code

More details (different callbacks, different uses) can be downloaded to debug the code yourself