Before reading this article, I recommend reading this article: JS(browser) event rings (macros, microtasks)
You need to understand how JS works and the order in which micro and macro tasks are executed
The sample
Let’s start with an example of DOM updates in Vue and what nextTick does.
< the template > < div id = "app" > < div ref = "MSG" > {{MSG}} < / div > < div > < button @ click = "change ()" > click < / button > < / div > < div v-if="msg1">msg1: {{msg1}}</div> <div v-if="msg2">msg2: {{msg2}}</div> <div v-if="msg3">msg3: </div> </div> </template> <script> export default {data() {return {MSG: "How are you?" , msg1: "", msg2: "", msg3: "" }; }, methods: {change() {this.msg = "I am not good!" ; Msg1 = this.msg1 = this.$refs.msg.innerhtml; $nextTick(() => {this.msg2 = this.$refs.msg.innerhtml; // use $nextTick to add MSG to msg2}); this.msg3 = this.$refs.msg.innerHTML; // get MSG data to msg3}}}; </script>Copy the code
Operation effect:
As you can see from the figure, msG1 and MSG3 still display the content before the transformation, while MSG2 shows the content after the transformation.
The root cause is that DOM updates in Vue are asynchronous (see below).
The principle of
NextTick API description:
A deferred callback is performed after the next DOM update loop ends. Use this method immediately after modifying the data to get the updated DOM.
Because the Vue implementation is responsive, the DOM does not change immediately after the data changes, but updates the DOM according to a certain strategy.
In the documentation for the Vue, it is stated that the Vue performs DOM updates asynchronously.
In simple terms, Vue does not update the view immediately after the data is changed. Instead, it updates the view after all the data changes in the same event loop are complete.
use
Application scenario: Operations need to be performed based on the new view after the view is updated.
In the Vue life cyclecreated()
DOM operations performed by hook functions must be placed inVue.nextTick()
In the callback function of
Because the DOM isn’t actually rendering at the time the Created () hook function is executed and DOM manipulation is useless, it’s important to put the DOM manipulation js code in the vue.nexttick () callback.
The mounted() hook function is the mounted() hook function, because it executes when all DOM operations are completed. Mounted does not promise that all child components will be mounted at once. If you want to wait until the entire view is rendered, replace Mounted with vm.$nextTick
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}
Copy the code
An operation to be performed after data changes that requires the use of a DOM structure that changes with the data should be placed in the vue.nexttick () callback. (As illustrated in the code example above)
Official note:
Vue performs DOM updates asynchronously. Whenever a data change is observed, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is triggered more than once, it will only be pushed into the queue once. This removal of duplicate data while buffering is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop, “TICK,” Vue refreshes the queue and performs the actual (de-duplicated) work. Vue internally tries to use native promise.then and MessageChannel for asynchronous queues, or setTimeout(fn, 0) instead if the execution environment does not support it.
For example, when you set vm.someData = ‘new value’, the component does not immediately re-render. When the queue is refreshed, the component updates with the next “tick” when the event loop queue is empty. In most cases we don’t need to worry about this process, but if you want to do something after a DOM status update, it can be tricky. While vue.js generally encourages developers to think in a “data-driven” way and avoid direct contact with the DOM, sometimes we do. To wait for Vue to finish updating the DOM after the data changes, use vue.nexttick (callback) immediately after the data changes. This callback will be called after the DOM update is complete.
The source code
Vue.nexttick is used to delay the execution of a piece of code. It takes two parameters (the callback function and the context in which the callback was executed) and returns a Promise object if no callback function is provided.
Version 2.5
/* @flow */ /* globals MessageChannel */ import { noop } from 'shared/util' import { handleError } from './error' import { isIOS, isNative } from './env' const callbacks = [] let pending = false function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; I ++) {copies[I]()}} // Use microtasks in 2.4, but there are still problems. // Here we have async deferring wrappers using both microtasks and (macro) tasks. 2.4 We use microtasks everywhere, but there are some scenarios where // microtasks have too high a priority and fire in between supposedly // sequential events (e.g. #4521, #6690) or even between bubbling of the same // event (#6566). However, using (macro) tasks everywhere also has subtle problems // when state is changed right before repaint (e.g. #6813, out-in transitions). // Here we use microtask by default, but expose a way to force (macro) task when // needed (e.g. in event handlers attached by v-on). // In version 2.5, there are two different variables in nextTick for calling microTask or macroTask. Let microTimerFunc SetImmediate -> MessageChannel -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate -> SetMediate SetTimeout (macro) Task defer implementation. SetTimeout (macro) Task defer implementation Technically setImmediate should be the ideal choice, but it's only available // in IE. The only polyfill that consistently queues the callback after all DOM // events triggered in the same loop is by using MessageChannel. /* istanbul ignore if */ if (typeof setImmediate ! == 'undefined' && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks) } } else if (typeof MessageChannel ! == 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) } } else { /* istanbul ignore next */ macroTimerFunc = () => { setTimeout(flushCallbacks, // macroTimerFunc -> macroTimerFunc -> macroTimerFunc // Determine microtask defer implementation. /* Istanbul ignore next, $flow-disable-line */ if (typeof Promise ! == 'undefined' && isNative(Promise)) { const p = Promise.resolve() microTimerFunc = () => { 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)}} else {// Fallback to macro microTimerFunc = macroTimerFunc ** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */ export function withMacroTask (fn: Function): Function { return fn._withTask || (fn._withTask = function () { useMacroTask = true try { return fn.apply(null, Arguments)} finally {useMacroTask = false}})} : Function, ctx? : Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (! pending) { pending = true if (useMacroTask) { macroTimerFunc() } else { microTimerFunc() } } // $flow-disable-line if (! cb && typeof Promise ! == 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }Copy the code
Version 2.6
/* @flow */ /* globals MutationObserver */ import { noop } from 'shared/util' import { handleError } from './error' import { isIE, isIOS, isNative } from './env' export let isUsingMicroTask = false const callbacks = [] let pending = false function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; I ++) {copies[I]()}} // Use a combination of microtasks and Macrotasks in version 2.5, but there are still some minor issues with redrawing, and using MacroTasks in the task queue has several particularly strange behaviors that cannot be avoided. So reverts to its previous state, prioritizing MicroTasks everywhere. // In 2.5 we use (macro) tasks (In combination with microtasks). // However, it has subtle problems when state is changed right before repaint // (e.g. #6813, out-in transitions). // Also, using (macro) tasks in event handler would cause some weird behaviors // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109). // So we now use microtasks everywhere, again. // A major drawback of this tradeoff is that there are some scenarios // where microtasks have too high a priority and fire in between supposedly // sequential events (e.g. #4521, #6690, which have workarounds) // or even between bubbling of the same event (#6566). let 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 stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore next, $flow-disabled-line () {// Promise -> MutationObserver -> setImmediate -> setTimeout if (typeof Promise! == 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { 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) let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate ! == 'undefined' && isNative(setImmediate)) { // Fallback to setImmediate. // Techinically it leverages the (macro) task queue, // but it is still a better choice than setTimeout. timerFunc = () => { setImmediate(flushCallbacks) } } else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) } } export function nextTick (cb? : Function, ctx? : Object) { let _resolve callbacks.push(() => { 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(resolve => { _resolve = resolve }) } }Copy the code