This time I will bring you a detailed explanation of the use of the Vue nextTick mechanism. What are the precautions when using the Vue nextTick mechanism. The following is a practical case, let's take a look.
Let’s first look at a piece of Vue execution code:
export default { data () { return { msg: 0 } }, mounted () { this.msg = 1 this.msg = 2 this.msg = 3 }, watch: { msg () { console.log(this.msg) } } }
We guess that after executing this script for 1000m, it will print: 1, 2, 3. But in actual effect, it will only be output once: 3. Why is there such a situation? Let’s find out.
queueWatcher
We define watch to listen to msg, which will actually be called by Vue like vm.$watch(keyOrFn, handler, options). $watch is a function bound to vm when we initialize it, used to create Watcher objects. Then let's take a look at how the handler is handled in Watcher:
this.deep = this.user = this.lazy = this.sync = false ... update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } ...
Initial setting this.deep = this.user = this.lazy = this.sync = false, that is, when an update is triggered, To execute the queueWatcher method:
const queue: Array<Watcher> = [] let has: { [key: number]: ?true } = {} let waiting = false let flushing = false ... export function queueWatcher (watcher: Watcher) { const 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. let 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 nextTick(flushSchedulerQueue) } } }
The flushSchedulerQueue function in nextTick(flushSchedulerQueue) is actually the watcher's ViewUpdate:
function flushSchedulerQueue () { flushing = true let watcher, id ... for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id has[id] = null watcher.run() ... } }
In addition, regarding the waiting variable, this It is a very important flag, which ensures that the flushSchedulerQueue callback is only allowed to be placed in callbacks once. Next, let's take a look at the nextTick function. Before talking about nexTick, you need to have a certain understanding of Event Loop, microTask, and macroTask. Vue nextTick also mainly uses these basic principles. If you don’t understand it yet, you can refer to my article Introduction to Event Loop. Now let’s take a look at its implementation:
export const nextTick = (function () { const callbacks = [] let pending = false let timerFunc function nextTickHandler () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } // An asynchronous deferring mechanism. // In pre 2.4, we used to use microtasks (Promise/MutationObserver) // but microtasks actually has too high a priority and fires in between // supposedly sequential events (e.g. #4521, #6690) or even between // bubbling of the same event (#6566). Technically setImmediate should be // the ideal choice, but it's not available everywhere; and 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)) { timerFunc = () => { setImmediate(nextTickHandler) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = () => { port.postMessage(1) } } else /* istanbul ignore next */ if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex const p = Promise.resolve() timerFunc = () => { p.then(nextTickHandler) } } else { // fallback to setTimeout timerFunc = () => { setTimeout(nextTickHandler, 0) } } return function queueNextTick (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, reject) => { _resolve = resolve }) } } })()
First, Vue simulates the event queue through callback array , events in the event team are called through the nextTickHandler method, and what is executed is determined by timerFunc. Let's take a look at the definition of timeFunc:
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(nextTickHandler) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = () => { port.postMessage(1) } } else /* istanbul ignore next */ if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex const p = Promise.resolve() timerFunc = () => { p.then(nextTickHandler) } } else { // fallback to setTimeout timerFunc = () => { setTimeout(nextTickHandler, 0) } }
You can see the priority of definition of timerFunc macroTask --> microTask, in an environment without Dom, use microTask, such as weex
setImmediate, MessageChannel VS setTimeout
We define setImmediate and MessageChannel first. Why should we use them first to create macroTask instead of setTimeout? HTML5 stipulates that the minimum time delay of setTimeout is 4ms, which means that under ideal circumstances, the fastest asynchronous callback can trigger is 4ms. Vue uses so many functions to simulate asynchronous tasks, with only one purpose, which is to make the callback asynchronous and called as early as possible. The delays of MessageChannel and setImmediate are obviously smaller than setTimeout.
Solution to the problem
With these foundations in mind, let’s look at the problems mentioned above again. Because Vue's event mechanism schedules execution through the event queue, it will wait for the main process to be idle before scheduling, so go back and wait for all processes to complete before updating again. This kind of performance advantage is obvious, for example:
Now there is a situation where the value of test will be looped executed 1000 times when mounted. Each time, setter->Dep->Watcher->update->run will be triggered responsively. If the view is not updated asynchronously at this time, the DOM will be directly manipulated to update the view every time, which is very performance consuming. Therefore, Vue implements a queue, and the run of the Watcher in the queue will be executed uniformly on the next Tick (or the microtask phase of the current Tick). At the same time, Watchers with the same ID will not be added to the queue repeatedly, so the Watcher run will not be executed 1,000 times. The final update of the view will only directly change the DOM corresponding to test from 0 to 1000. It is guaranteed that the action of updating the view to operate the DOM is called at the next Tick (or the microtask phase of the current Tick) after the current stack is executed, which greatly optimizes performance.
Interesting question
var vm = new Vue({ el: '#example', data: { msg: 'begin', }, mounted () { this.msg = 'end' console.log('1') setTimeout(() => { // macroTask console.log('3') }, 0) Promise.resolve().then(function () { //microTask console.log('promise!') }) this.$nextTick(function () { console.log('2') }) } })
Everyone must know the execution order of this and print it in sequence: 1, promise, 2, 3.
Because this.msg = 'end' is triggered first, the watcher's update is triggered, thereby pushing the update operation callback into the vue event queue.
this.$nextTick also enters a new callback function for event queue push. They all come through setImmediate --> MessageChannel --> Promise --> setTimeout Define timeFunc. Promise.resolve().then is a microTask, so it will print the promise first.
When MessageChannel and setImmediate are supported, their execution order takes precedence over setTimeout (in IE11/Edge, the setImmediate delay can be within 1ms, while setTimeout has a minimum delay of 4ms, so setImmediate executes the callback function earlier than setTimeout(0). Secondly, because in the event queue, the callback array is received first), so it will print 2, and then print 3
but In the case where MessageChannel and setImmediate are not supported, timeFunc will be defined through Promise, and the old version of Vue before 2.4 will execute promise first. This situation will cause the order to become: 1, 2, promise, 3. Because this.msg must first trigger the dom update function, the dom update function will first be collected by the callback into the asynchronous time queue, and then Promise.resolve().then(function () { console.log('promise!')} will be defined. ) such a microTask, and then defining $nextTick will be collected by the callback. We know that the queue satisfies the first-in-first-out principle, so the objects collected by the callback are executed first.
Postscript
If you are interested in Vue source code, you can come here: More interesting Vue convention source code explanations
I believe you have mastered the method after reading the case in this article. For more exciting information, please pay attention to other related articles on the php Chinese website!
Recommended reading:
JS to implement transparency gradient function
##jQuery traversal of XML nodes and attributes implementation steps
The above is the detailed content of Detailed explanation of the use of Vue nextTick mechanism. For more information, please follow other related articles on the PHP Chinese website!