The official definition of

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

  • Details:

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.

For the first time to explore

Our intention is to monitor the app variable and set a breakpoint in the function. What we expect is that when the breakpoint stops, the relevant function will appear on the call stack, providing us with a basis for analyzing the watch principle.

With the above intentions and expectations in mind, let’s create a new Vue project and write the following code:

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

After refreshing the page, the call stack on the right is displayed as 👇 :

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

See the need to go through so much call process, can not help heart a panic… However, if you understand the last article on computed tomography, you can easily see that:

Vue uses dependency collection on variables to alert them when their values change. Finally, computed depending on this variable determines whether recalculation or caching is required

Computed is somewhat similar to watch, so when we see reactiveSetter, we probably think that Watch must also use dependency collection.

Why was it executed?queueWatcher

Looking at the call stack alone, the watch procedure executes queueWatcher, which is placed in the update

Update implementation 👇 :

/** * 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); }};Copy the code

Obviously, the queueWatcher function depends on these two variables:

  • this.lazy
  • this.sync

These two variables are actually initialized in the Watcher class, so here’s a breakpoint, and here’s the call order 👇 :

  • initWatch
  • createWatcher
  • Vue.$watch
  • Watcher
initWatch👇
function initWatch (vm, watch) {
  // Iterate over the watch property
  for (var key in watch) {
    var handler = watch[key];
    // If it is an array, iterate again
    if (Array.isArray(handler)) {
      for (var i = 0; i < handler.length; i++) {
        / / call createWatchercreateWatcher(vm, key, handler[i]); }}else {
      / / same as abovecreateWatcher(vm, key, handler); }}}Copy the code
createWatcher👇
function createWatcher (vm, expOrFn, handler, options) {
   // Get the property again when the value is an object
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  // Compatible character types
  if (typeof handler === 'string') {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options)
}
Copy the code
Vue.prototype.$watch👇
Vue.prototype.$watch = function (expOrFn, cb, options) {
	var vm = this;
	// If cb is an object, call createWatcher again
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
	options.user = true;
	// Create an instance of Watcher
	var watcher = new Watcher(vm, expOrFn, cb, options);
	// If you set immediate to true in the watch object, execute it immediately
    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(); }};Copy the code
summary

The initialization process for Watch is relatively simple, and the comments above are clear enough. Of course, the previously mentioned this.lazy and this.sync variables, since no true value was passed in during initialization, go directly to the queueWatcher function when update is triggered

Further study of

queueWatcherThe implementation of the

/** * 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;
  // Check whether it is already in the queue to prevent repeated triggering
  if (has[id] == null) {
    has[id] = true;
	// The wacher is queued without refreshing the queue
    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.
      // If you are refreshing, the watcher will be inserted in order of id
      // If the watcher has been refreshed, it will be executed again on the next refresh
      var i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1.0, watcher);
    }
    // queue the flush
    // Queue to refresh
    if(! waiting) { waiting =true;

      FlushSchedulerQueue is called in the development environment with async set to false
      if(process.env.NODE_ENV ! = ='production' && !config.async) {
        flushSchedulerQueue();
        return
      }
      Otherwise, call flushSchedulerQueue in nextTicknextTick(flushSchedulerQueue); }}}Copy the code

QueueWatcher is an important function, and we can extract some key points from the above code 👇

  • rightwatcher.idDo to reprocess, for simultaneous triggeringqueueWatcherIn the samewatcherAnd onlypushOne enters the queue
  • An asynchronous refresh queue (flashSchedulerQueue) NexttickIs executed at the same timewaitingVariable to avoid repeated calls
  • If triggered during the refresh phasequeueWatcher, then press itidInserts into the queue in ascending order; If it has already been refreshed, it will be executed immediately on the next call to the queue
How do I understand triggering in the refresh phasequeueWatcherThe operation?

This is not too hard to understand. We put breakpoints into the flushSchedulerQueue, and only the simplified code 👇 is listed here

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow();
  flushing = true;
  varwatcher, id; . for (index =0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null; watcher.run(); . }... }Copy the code

There are two key variables:

  • fluashing
  • has[id]

All changes before watcher.run(). This means that before/during the refresh queue phase of the corresponding watch function, other variables can be re-added to the refresh queue during this refresh phase

Finally, put the full code:

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

  // Sort the queue once before refreshing
  // This operation guarantees that:
  // 1. Components are updated from parent components to child components (because parent components are always created before child components)
  // 2. A component custom Watchers is executed before its render Watcher.
  // 3. If a component was destroyed during watcher execution of the parent component, the Watchers were skipped
  queue.sort(function (a, b) { return a.id - b.id; });

  // Do not cache the length of the queue, because new watcher may be added to the queue during the refresh phase
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    // Execute the method defined in watch
    watcher.run();
    // In the test environment, do special handling for possible dead loops and give hints
    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}}}// Make a shallow copy of activatedChildren and Queue before resetting the status.
  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();

  // Reset the state of the timer, that is, the state of the has, waiting, and flushing variables in the asynchronous refresh
  resetSchedulerState();

  // Invokes the component's updated and activated hooks
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);

  // Deltools hooks
  if (devtools && config.devtools) {
    devtools.emit('flush'); }}Copy the code

nextTick

The asynchronous flushSchedulerQueue is actually executed in nextTick. Here we briefly analyze the implementation of nextTick. The specific code is as follows: 👇

// Two arguments, one cb (callback) and one CTX (context object)
function nextTick (cb, ctx) {
  var _resolve;
  // Put the destroy function into the Callbacks array
  callbacks.push(function () {
    if (cb) {
      try {
        // Call the callback
        cb.call(ctx);
      } catch (e) {
        // Catch an error
        handleError(e, ctx, 'nextTick'); }}else if (_resolve) { // If cb does not exist, call _resolve_resolve(ctx); }});if(! pending) { pending =true;
    timerFunc();
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(function (resolve) { _resolve = resolve; }}})Copy the code

We see that a timeFunc function is actually called here (sorry, the code comment is not 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 &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  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 (typeofsetImmediate ! = ='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);
  };
}
Copy the code

The timerFunc code is actually quite simple and does these things:

  • Check the browser forPromise,MutationObserver,setImmediateAnd select them in descending order of priority
    1. Promise
    2. MutationObserver
    3. setImmediate
    4. setTimeout
  • In support ofPromise / MutationObserverCan be triggered in the case ofMicro tasks(microTask) can only be used when compatibility is poorsetImmediate / setTimeoutThe triggerMacro task(macroTask)

Of course, the concept of macroTask and microTask will not be elaborated here, as long as we know that in the asynchronous task execution process, under the same starting line, microTask priority is always higher than macroTask.

tips
  1. Global search can actually be foundnextTickThis method is bound to theVueOn the prototype 👇
Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)};Copy the code
  1. nextTickCan not be arbitrarily tuned to 👇
if(! pending) { pending =true;
  timerFunc();
}
Copy the code

conclusion

  • watchwithcomputedLikewise, depend onVueThe responsive system of the
  • For an asynchronous refresh queue (flushSchedulerQueue), there can be new before/after the refreshwatcherEnter the queue, of course, ifnextTickPerform before
  • withcomputedThe difference is,watchNot immediately, but the next onetickExecute in, that isMicro tasks(microTask) / Macro task(macroTask)

Scan the QR code below or search for “Teacher Tony’s front-end cram school” to follow my wechat official account, and then you can receive my latest articles in the first time.