场景阐明
最近应用 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 做了什么呢?这里以下面的代码为例,咱们先来理一理咱们代码是怎么执行的。具体来说,以上代码执行程序如下:
- form.a 初始值为 null
- 用户输出字符串 abc
- 触发 input 事件,form.a 的值改为 abc
- 触发 on-change 事件,form.a 的值改为 null
- 因为 form.a 的值到这里还是为 null
- 主线程工作执行结束,查看 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,如有侵权,请分割删除。