乐趣区

关于android:nextTick的理解和作用

场景阐明

最近应用 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,如有侵权,请分割删除。

退出移动版