Look at the watch of the Vue source code according to the debugging tool

Look at the watch of the Vue source code according to the debugging tool

Official definition

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

  • Details :

For an object, the key is the expression to be observed, and the value is the corresponding callback function. The value can also be a method name, or an object containing options. The Vue instance will call $watch() when it is instantiated to traverse every attribute of the watch object.

First exploration

Our intention is to monitor appthis variable and place a breakpoint in the function.
What we expect is that after the breakpoint stops, related functions appear in the call stack, providing watcha basis for our analysis of the principle.

With the above intentions and expectations, we create a new Vueproject and write the following code at the same time:

created () {
    this.app = 233
},
watch: {
    app (val) {
      debugger
      console.log('val:', val)
    }
}
  

After refreshing the page, the call stack on the right is shown as follows :

  • app
  • run
  • flushSchedulerQueue
  • anonymous
  • flushCallbacks
  • timeFunc
  • nextTick
  • queueWatcher
  • update
  • notify
  • reactiveSetter
  • proxySetter
  • created
  • ...

Seeing the need to go through so many calls, I can t help feeling panic... However, if you understand the previous article about it computed, you can easily know:

VueBy relying on the collection of variables, a message will be reminded when the value of the variable changes. Finally, the computedfinal decision that depends on the variable is to recalculate or use the cache

computedIt watchis still somewhat similar, so reactiveSetterwhen we saw it, we probably thought in our hearts that we watchmust also use dependency collection .

Why is the queueWatcher executed

If you look at the call stack alone, this watchprocess is executed queueWatcher, and this function is placed updatein

updateThe realization of :

/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
Watcher.prototype.update = function update () {
 /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};
  

Obviously, queueWatcherwhether the function is called depends on these two variables:

  • this.lazy
  • this.sync

These two variables are actually Watcherinitialized in the class, so breakpoints are set here, and the calling sequence is directly given below:

  • initWatch
  • createWatcher
  • Vue.$watch
  • Watcher
initWatch
function initWatch (vm, watch) {
 // watch 
  for (var key in watch) {
    var handler = watch[key];
   // 
    if (Array.isArray(handler)) {
      for (var i = 0; i < handler.length; i++) {
       // createWatcher
        createWatcher(vm, key, handler[i]);
      }
    } else {
     // 
      createWatcher(vm, key, handler);
    }
  }
}
  
createWatcher
function createWatcher (
  vm,
  expOrFn,
  handler,
  options
) {
  // 
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
 // 
  if (typeof handler === 'string') {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options)
}
  
Vue.prototype.$watch
Vue.prototype.$watch = function (
    expOrFn,
    cb,
    options
  ) {
	var vm = this;
	// cb createWatcher
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
	options.user = true;
	// Watcher 
	var watcher = new Watcher(vm, expOrFn, cb, options);
	// watch immediate true 
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value);
      } catch (error) {
        handleError(error, vm, ("callback for immediate watcher/"" + (watcher.expression) + "\""));
      }
    }
    return function unwatchFn () {
      watcher.teardown();
    }
  };
  
summary

watchThe initialization process is relatively simple, and the comments given above are clear enough. Of course, the aforementionedthis.lazy and this.syncvariables, because there is no truevalue passed in during the initialization process , they updatewalk directly into the queueWatcherfunction when triggered

In-depth study

Implementation of queueWatcher

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
function queueWatcher (watcher) {
  var id = watcher.id;
 // 
  if (has[id] == null) {
    has[id] = true;
	// wacher 
    if (!flushing) {
      queue.push(watcher);
    } else {
     //if already flushing, splice the watcher based on its id
     //if already past its id, it will be run next immediately.
     // watcher id 
     // watcher 
      var i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
   //queue the flush
   // 
    if (!waiting) {
      waiting = true;

     // async false flushSchedulerQueue
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue();
        return
      }
     // nextTick flushSchedulerQueue
      nextTick(flushSchedulerQueue);
    }
  }
}
  

queueWatcherIt is a very important function, from the above code we can extract some key points

  • To watcher.iddo de-duplication processing, for the same triggered queueWatcherat the same timewatcher , only pushone enters the queue
  • An asynchronous refresh queue ( flashSchedulerQueue) in the nexttick is executed , and waitingvariables are used at the same time to avoid repeated calls
  • If triggered during the refresh phasequeueWatcher , it will idbe inserted into the queue in order from small to large; if it has been refreshed, it will be executed immediately in the next call of the queue
How to understand the operation of triggering queueWatcher during the refresh phase?

In fact, it is not difficult to understand this, we will enter the breakpoint flushSchedulerQueue, here is only the simplified code

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;

  ...

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    watcher.run();
    
    ...
  }

  ...
}
  

Two of the key variables:

  • fluashing
  • has[id]

All watcher.run()changes before. This means that, in the corresponding watchfunction before execution/execution (At this stage in the refresh queue), other variables can be refreshed at this stage to re-join the queue refresh

Finally put the complete code:

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;

 // 
 // 
 //1.  
 //2.  watchers watcher watchers watchers render watcher 
 //3.  watcher watchers 
  queue.sort(function (a, b) { return a.id - b.id; });

 // watcher 
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
   // watch 
    watcher.run();
   // 
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1;
      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
      }
    }
  }

 // activatedChildren queue 
  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();

 // has waiting flushing 
  resetSchedulerState();

 //  updated   activated  
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);

 //deltools  
  if (devtools && config.devtools) {
    devtools.emit('flush');
  }
}
  

nextTick

The asynchronous refresh queue ( flushSchedulerQueue) is actually nextTickexecuted in, here we briefly analyze nextTickthe implementation, the specific code is as follows

// cb ctx 
function nextTick (cb, ctx) {
  var _resolve;
 // callbacks 
  callbacks.push(function () {
    if (cb) {
      try {
       // 
        cb.call(ctx);
      } catch (e) {
       // 
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {// cb _resolve
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    timerFunc();
  }
 //$flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve;
    })
  }
}
  

We see that there is actually a timeFuncfunction called here (be lazy, the comment of this code will not be translated)

var timerFunc;

//The nextTick behavior leverages the microtask queue, which can be accessed
//via either native Promise.then or MutationObserver.
//MutationObserver has wider support, however it is seriously bugged in
//UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
//completely stops working after triggering a few times... so, if native
//Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  timerFunc = function () {
    p.then(flushCallbacks);
   //In problematic UIWebViews, Promise.then doesn't completely break, but
   //it can get stuck in a weird state where callbacks are pushed into the
   //microtask queue but the queue isn't being flushed, until the browser
   //needs to do some other work, e.g. handle a timer. Therefore we can
   //"force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) { setTimeout(noop); }
  };
  isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
 //PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
 //Use MutationObserver where native Promise is not available,
 //e.g. PhantomJS, iOS7, Android 4.4
 //(#6466 MutationObserver is unreliable in IE11)
  var counter = 1;
  var observer = new MutationObserver(flushCallbacks);
  var textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true
  });
  timerFunc = function () {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
 //Fallback to setImmediate.
 //Techinically it leverages the (macro) task queue,
 //but it is still a better choice than setTimeout.
  timerFunc = function () {
    setImmediate(flushCallbacks);
  };
} else {
 //Fallback to setTimeout.
  timerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}
  

timerFuncThe code is actually very simple, nothing more than doing these things:

  • Check for the browser Promise, MutationObserver, setImmediatecompatibility, according to the descending order of priority were selected
    1. Promise
    2. MutationObserver
    3. setImmediate
    4. setTimeout
  • In support Promise/MutationObservercase can trigger the micro-task ( microTask), can only be used when poor compatibility setImmediate/setTimeouttrigger macro task ( macroTask)

Of course, on the macro task ( macroTask) and micro-task ( microTaskconcept) here is not elaborated on, as long as we know, in the course of the asynchronous task execution, at the same starting line, micro-task ( microTask) is always higher than the priority of macro task ( macroTask).

tips
  1. Global search can actually find that nextTickthis method is bound to Vuethe prototype
Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)
};
  
  1. nextTickIt cannot be arbitrarily adjusted
if (!pending) {
  pending = true;
  timerFunc();
}
  

summary

  • watchWith computedthe same, relying on the Vueresponsive system
  • For an asynchronous refresh queue ( flushSchedulerQueue), there can be new watcherentries into the queue before/after refresh , of course, the premise is nextTickbefore execution
  • The computeddifference is that watchit is not executed immediately, but executed in the next one tick, that is, micro task ( microTask)/macro task ** ( macroTask)