The scene that

Recently, when using Vue bucket as a backend system, I encountered a bizarre problem: there was an input field that only allowed numbers to be entered, and when entering other types of data, the input would be reset to NULL. To achieve this, a parent component and a child component are used. For the convenience of presentation, the business scenario is simplified as follows:

/ / the parent component
<template>
  <Input v-model="form.a" @on-change="onChange"></Input>
</template>
<script type="javascript">
export default {
	data() {
    return {
      form: {
        a: null}}},methods: {
    async onChange(value) {
   		if (typeofvalue ! = ='number') {
        // await this.$nextTick()
        this.form.a = null}}}}</script>

/ / child component
<template>
  <input v-model="currentValue" @input="onInput" />
</template>
<script type="javascript">
export default {
	name: 'Input'.props: {
    value: {
      type: [Number, Null],
      default: null}},data() {
    return {
      currentValue: null}},methods: {
    onInput(event) {
      const value = event.target.value
      this.$emit('input', value)
      const oldValue = this.value
      if (oldValue === value) return
   		this.$emit('on-change', value)
    }
  },
  watch: {
    value(value, oldValue) {
      this.currentValue = value
    }
  }
}
</script>
Copy the code

Run the above code in a project and you will be surprised to see that after the string ‘ABC’ is entered in the input field, the value of the input field is not reset to null, but remains unchanged as’ ABC ‘. After the nextTick of the comment is uncommented, the value of the input box is reset to null. It’s really amazing.

Several times before, my colleagues had encountered a similar scenario: the data layer changed and the DOM didn’t respond. After the data layer changes, execute nextTick once, and the DOM updates as expected. After doing this a few times, we even joked about nextTick.

Code execution order

So what does nextTick do? Using the above code as an example, let’s take a look at how our code executes. Specifically, the above code is executed in the following order:

  1. The form.a initial value is null
  2. The user enters the string ABC
  3. Raises the input event, and the form.a value changes to ABC
  4. The on-change event is raised, and the form.a value is changed to NULL
  5. Since the form.a value is still null up here
  6. After executing the main thread task, check whether the watch callback function needs to be executed.

In this order, we can see why ABC is not null in the input box: the form.a value changes in the middle of the main thread, but it is always null at the beginning and end. That is, the value of the props of the child component has not changed. Naturally, the watch callback will not execute.

But then we have another question: why is the watch callback not triggered when the input event is triggered and the form.a value is changed to NULL? To illustrate this, we need to dig into the Vue source code to see when the $EMIT and Watch callbacks are executed, respectively.

What does $emit do?

Let’s first look at the source code for $emit. Since the Vue 2.X version of the source is written using flow, the cost of understanding is virtually increased. With this in mind, we went straight to the vue.js file in Vue’s dist package and searched for the EMIT function

Vue.prototype.$emit = function (event) {
  var vm = this;
  {
    var lowerCaseEvent = event.toLowerCase();
    if(lowerCaseEvent ! == event && vm._events[lowerCaseEvent]) { tip("Event \"" + lowerCaseEvent + "\" is emitted in component " +
        (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\"." +
        "Note that HTML attributes are case-insensitive and you cannot use " +
        "v-on to listen to camelCase events when using in-DOM templates. " +
        "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."); }}var cbs = vm._events[event];
  if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs;
    var args = toArray(arguments.1);
    var info = "event handler for \"" + event + "\" ";
    for (var i = 0, l = cbs.length; i < l; i++) { invokeWithErrorHandling(cbs[i], vm, args, vm, info); }}return vm
};

function invokeWithErrorHandling (handler, context, args, vm, info) {
  var res;
  try {
    res = args ? handler.apply(context, args) : handler.call(context);
    if(res && ! res._isVue && isPromise(res) && ! res._handled) { res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true; }}catch (e) {
    handleError(e, vm, info);
  }
  return res
}
Copy the code

The content of the source code is actually very simple, is to pre-register (or subscribe) functions into an array, when executing the $emit function to fetch the array functions and execute. As you can see, this is the use of a publish-subscribe model.

That is, emit execution is synchronous. So how does Watch work? Watch, by contrast, is cumbersome to implement. Once you understand the flow of Watch, you understand the core of Vue.

First, when initializing the Vue component, there is an initWatch function, so let’s see what this function does.

function initWatch (vm, watch) {
  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); }}}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 = function (expOrFn, cb, options) {
  var vm = this;
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {};
  options.user = true;
  var watcher = new Watcher(vm, expOrFn, cb, options);
  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(); }}var Watcher = function Watcher (vm, expOrFn, cb, options, isRenderWatcher) {
  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();
  // parse expression for getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop;
      warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.', vm ); }}this.value = this.lazy
    ? undefined
    : this.get();
}

function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  var segments = path.split('. ');
  return function (obj) {
    for (var i = 0; i < segments.length; i++) {
      if(! obj) {return }
      obj = obj[segments[i]];
    }
    return obj
  }
}

Watcher.prototype.get = function get () {
  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 {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
}

function defineReactive$$1 (obj, key, val, customSetter, shallow) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key];
  }

  varchildOb = ! shallow && observe(val);Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) { dependArray(value); }}}return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if (customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if(getter && ! setter) {return }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}

var Dep = function Dep () {
  this.id = uid++;
  this.subs = [];
}

Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};

Dep.prototype.removeSub = function removeSub (sub) {
  remove(this.subs, sub);
};

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this); }}; Dep.prototype.notify =function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  if(! config.async) {// subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
}

Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
}

Dep.target = null;
var targetStack = [];

function pushTarget (target) {
  targetStack.push(target);
  Dep.target = target;
}

function popTarget () {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}
Copy the code

As we can see, the functions associated with watch are close to 20. With so many functions bouncing back and forth, it’s easy to lose logic. Let’s go through the process here.

When initializing the Vue instance, initWatch is executed, and the initWatch function goes down to create an instance of Watcher. The Watcher instance executes the getter function, which reads the value of a property of data, thus firing the get function in Object.defineProperty. The get function executes the dep.depend function, which is used to collect dependencies. A dependency is a callback function. In this case, the watch callback for value.

At this point, we see that the watch callback function is only registered here and has not yet been executed. So where is the real implementation of Watch? Let’s go back to the original code execution order. In step 3, form.a= ABC, there is a set action. This action triggers the set function of Object.defineProperty, which executes the dep.notify function. The update function, the core of which is the queueWatcher function, is executed. To better illustrate, let’s break out the queueWatcher function.

function queueWatcher (watcher) {
  var id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    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.
      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;

      if(! config.async) { flushSchedulerQueue();return} nextTick(flushSchedulerQueue); }}}function flushSchedulerQueue () {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  // created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  // user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  // its watchers can be skipped.
  queue.sort(function (a, b) { return a.id - b.id; });

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    watcher.run();
    // in dev build, check and stop circular updates.
    if(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}}}// keep copies of post queues before resetting state
  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();

  resetSchedulerState();

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush');
  }
}

Watcher.prototype.run = function run () {
  if (this.active) {
    var value = this.get();
    if( value ! = =this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      var oldValue = this.value;
      this.value = value;
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\" ")); }}else {
        this.cb.call(this.vm, value, oldValue); }}}}function nextTick (cb, ctx) {
  var _resolve;
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick'); }}else if(_resolve) { _resolve(ctx); }});if(! pending) { pending =true;
    timerFunc();
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(function (resolve) { _resolve = resolve; }}})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.
  // Technically 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

In the queueWatcher function, we see a familiar face: nextTick. We find that nextTick is a smooth degradation of a microtask: It uses Promise, MutationObserver, setImmediate, and setTimeout to perform the task, in turn, depending on the context. We see that when we execute form.a= ABC, we actually register a microtask first, which we can think of as a wrapper function for the watch callback function. The microtask will be executed after the main thread task has finished, so it will be suspended first.

The main thread then executes form.a=null, triggering the setter again. Since all forms are registered by form.a, the task will be removed before being pushed into the micro-task queue to avoid multiple execution of the watch callback. At this point, the main thread task completes, and the wrapper function of the Watcher callback function in the microtask queue is pushed out to execute. Since the form.a value is always null, the callback function is not executed.

$nextTick =null; the nextTick function wraps watcher’s callback function, and the form.a value is ABC. The old value is different from the new value, so the watch callback is executed. At this point, the whole logic is clear.

The latter

Surprisingly, a simple use of nextTick is related to the core principles of Vue!