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