场景阐明

最近应用Vue全家桶做后盾零碎的时候,遇到了一个很奇葩的问题:有一个输入框只容许输出数字,当输出其它类型的数据时,输出的内容会被重置为null。为了实现这一性能,应用了一个父组件和子组件。为了不便陈说,这里将业务场景简化,具体代码如下:

// 父组件<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 (typeof value !== 'number') {        // await this.$nextTick()        this.form.a = null      }       }  }}</script>// 子组件<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>

将以上代码放到我的项目中去运行,你会很神奇地发现,在输入框输出字符串'abc'之后,输入框的值并没有被重置为空,而是放弃为'abc'没有变动。在将正文的nextTick勾销正文当前,输入框的值被重置为空。真的十分神奇。

其实之前有好几次共事也碰到了相似的场景:数据层产生了变动,dom并没有随之响应。在数据层发生变化当前,执行一次nextTick,dom就依照预期地更新了。这样几次当前,咱们甚至都调侃:遇事不决nextTick。

代码执行程序

那么,到底nextTick做了什么呢?这里以下面的代码为例,咱们先来理一理咱们代码是怎么执行的。具体来说,以上代码执行程序如下:

  1. form.a初始值为null
  2. 用户输出字符串abc
  3. 触发input事件,form.a的值改为abc
  4. 触发on-change事件,form.a的值改为null
  5. 因为form.a的值到这里还是为null
  6. 主线程工作执行结束,查看watch的回调函数是否须要执行。

这个程序一理,咱们就发现了输入框展现abc不置空的起因:原来form.a的值在主线程两头尽管产生了变动,然而最开始到最初始终为null。也就是说,子组件的props的value没有发生变化。天然,watch的回调函数也就不会执行。

然而这样一来,咱们就有另外一个问题了:为什么触发input事件,form.a的值改为null的时候,没有触发watch的回调呢?为了阐明这一点,咱们须要深刻Vue源码,看看$emit和watch的回调函数别离是在什么时候执行的。

$emit做了什么?

咱们首先看看$emit对应的源码。因为Vue 2.X版本源码是应用flow写的,无形中减少了了解老本。思考到这一点,咱们间接找到Vue的dist包中的vue.js文件,并搜寻emit函数

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}

源码的内容其实很简略,就是把提前注册(或者说订阅)的函数放到一个数组中,执行$emit函数时就把数组中的函数一一取出并执行。能够看出,这是一个公布-订阅模式的应用。

也就是说,emit的执行是同步的。那么,watch是怎么执行的呢?相比之下,watch的执行会比拟繁琐。了解了watch的流程,也就了解了Vue的外围。

首先,在初始化Vue组件时,有一个initWatch函数,咱们来看看这个函数做了什么。

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];  }  var childOb = !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];}

咱们看到,watch相关联的函数靠近20个。这么多函数在来回跳的时候,很容易把逻辑弄丢了。这里咱们来讲一讲整个流程。

在初始化Vue实例时,执行initWatch,initWatch函数往下走,创立了一个watcher实例。watcher实例执行了getter函数,getter函数读取了data某个属性的值,因而触发了Object.defineProperty中的get函数。get函数执行了dep.depend函数,这个函数用于收集依赖。所谓的依赖其实就是回调函数。在咱们说的这个例子中,就是value的watch回调函数。

讲到这里,咱们发现watch的回调函数只是在这里进行了注册,还没有执行。那么,watch真正的执行是在哪里呢?咱们回到最开始代码的执行程序来看。在第3步的时候,form.a=abc,这里有一个设置的操作。这个操作触发了Object.defineProperty的set函数,set函数执行了dep.notify函数。执行了update函数,update函数的外围就是queueWatcher函数。为了更好地阐明,咱们把queueWatcher函数独自列出来看看。

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 && 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.  // 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);  };}

在queueWatcher函数中,咱们看到了相熟的脸孔:nextTick。咱们发现,nextTick就是一个微工作的安稳降级:它将依据所在环境,顺次应用Promise、MutationObserver、setImmediate以及setTimeout执行工作。咱们看到,执行form.a=abc时,实际上是先注册了一个微工作,在这里咱们能够了解为watch回调函数的包裹函数。这个微工作将在主线程工作走完当前执行,因而它将被先挂起。

随后主线程执行了form.a=null,再次触发了setter。因为都是form.a注册的,在推入微工作队列前会去重,防止watch的回调屡次执行。到这里,主线程工作执行实现,微工作队列中watcher回调函数的包裹函数被推出执行,因为form.a的值始终都为null,因而不会执行回调函数。

在退出$nextTick函数当前,在form.a=null之前先执行了nextTick函数,nextTick函数执行了watcher的回调函数的包裹函数,此时form.a的值为abc,旧的值和新的值不一样,因而执行了watch回调函数。至此,整个逻辑就理顺了。

相干教程

Android根底系列教程:
Android根底课程U-小结_哔哩哔哩_bilibili
Android根底课程UI-布局_哔哩哔哩_bilibili
Android根底课程UI-控件_哔哩哔哩_bilibili
Android根底课程UI-动画_哔哩哔哩_bilibili
Android根底课程-activity的应用_哔哩哔哩_bilibili
Android根底课程-Fragment应用办法_哔哩哔哩_bilibili
Android根底课程-热修复/热更新技术原理_哔哩哔哩_bilibili

后话

没想到,一个简简单单nextTick的应用竟然关系到了Vue的外围原理!

本文转自 https://juejin.cn/post/6976246978850062367,如有侵权,请分割删除。