Scenario description
Recently, when using Vue as the background system, I encountered a very wonderful problem: there is an input box that allows only numbers to be entered. When other types of data are entered, the entered content will be reset to null. In order to achieve this function, a parent component and a child component are used. To facilitate the presentation, the business scenario is simplified here. The specific code is as follows:
// 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 (typeof value !== 'number') { // await this.$nextTick() this.form.a = null } } } } </script> // Subcomponents <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>
Put the above code into the project and run it. You will magically find that after entering the string 'abc' in the input box, the value of the input box is not reset to empty, 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.
In fact, colleagues have encountered similar scenarios several times before: the data layer has changed, and the dom has not responded accordingly. After the data layer changes, execute nextTick once, and the dom updates as expected. After such a few times, we even joked about nextTick.
Code execution sequence
So, what did nextTick do? Here, taking the above code as an example, let's first understand how our code is executed. Specifically, the execution sequence of the above codes is as follows:
- form. The initial value of a is null
- User input string abc
- Trigger the input event, form Change the value of a to abc
- Trigger the on change event, form Change the value of a to null
- Due to form The value of a is still null here
- After the main thread task is executed, check whether the callback function of watch needs to be executed.
Once this sequence is sorted out, we find the reason why the input box shows that abc is not empty: the original form Although the value of a has changed in the main thread, it is always null from the beginning to the end. That is, the value of the props of the subcomponent does not change. Naturally, the callback function of watch will not be executed.
However, in this way, we have another question: why does the input event, form When the value of a is changed to null, does it not trigger the callback of watch? To illustrate this, we need to go deep into the Vue source code to see when the callback functions of $emit and watch are executed respectively.
What did $emit do?
Let's first look at the source code corresponding to $emit. Due to Vue 2 The source code of version x is written using flow, which virtually increases the understanding cost. With this in mind, we directly find Vue in Vue's dist package JS file and search 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 }
The content of the source code is actually very simple, that is, put the functions registered (or subscribed) in advance into an array, and take out and execute the functions in the array one by one when executing the $emit function. It can be seen that this is the use of publish subscribe mode.
In other words, the execution of emit is synchronous. So, how does watch execute? In contrast, the implementation of watch is cumbersome. Understanding the process of watch means understanding the core of Vue.
First, when initializing Vue components, there is an initWatch function. 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]; } 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]; }
We see that there are nearly 20 functions associated with watch. It's easy to lose logic when so many functions jump back and forth. Let's talk about the whole process.
When initializing the Vue instance, execute initWatch. The initWatch function goes down and creates a watcher instance. The watcher instance executes the getter function. The getter function reads the value of a property of data, so it triggers object Get function in defineproperty. The get function executes the dep.depend function, which is used to collect dependencies. The so-called dependency is actually a callback function. In our example, it is the watch callback function of value.
At this point, we find that the callback function of watch is only registered here and has not been executed yet. So, where is the real implementation of watch? Let's go back to the execution order of the initial code. In step 3, form A = ABC, here is a setting operation. This operation triggers object The set function of defineproperty executes the dep.notify function. The update function is executed, and the core of the update function is the queueWatcher function. For better illustration, let's list the queueWatcher function separately.
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); }; }
In the queueWatcher function, we see the familiar face: nextTick. We found that nextTick is a smooth degradation of micro tasks: it will use Promise, MutationObserver, setImmediate and setTimeout to execute tasks according to its environment. As we can see, execute form When a = ABC, a micro task is actually registered first. Here, we can understand it as the package function of the watch callback function. This micro task will be executed after the main thread task is completed, so it will be suspended first.
The main thread then executes the form A = null, the setter is triggered again. Because they are all forms A registered will be de duplicated before pushing into the micro task queue to avoid multiple executions of the watch callback. Here, the main thread task execution is completed, and the package function of the watcher callback function in the micro task queue is pushed out for execution The value of a is always null, so the callback function will not be executed.
After adding the $nextTick function, in form The nextTick function is executed before a = null. The nextTick function executes the wrapper function of the callback function of the watcher. At this time, form The value of a is abc. The old value is different from the new value, so the watch callback function is executed. So far, the whole logic has been straightened out.
Related tutorials
Android Foundation Series tutorials:
Android foundation course U-summary_ Beep beep beep_ bilibili
Android foundation course UI layout_ Beep beep beep_ bilibili
Android basic course UI control_ Beep beep beep_ bilibili
Android foundation course UI animation_ Beep beep beep_ bilibili
Android basic course - use of activity_ Beep beep beep_ bilibili
Android basic course - Fragment usage_ Beep beep beep_ bilibili
Android basic course - Principles of hot repair / hot update technology_ Beep beep beep_ bilibili
Later words
Unexpectedly, the use of a simple nextTick is actually related to the core principle of Vue!
This article is transferred from https://juejin.cn/post/6976246978850062367 , in case of infringement, please contact to delete.