One of the most common interview questions these days is: When using vue, increment the variable I declared in the for loop from 1 to 100, and then display I on the page, and the I on the page jumps from 1 to 100, or what? The answer, of course, is that only 100 is displayed, and there is no jump.
How can a page display from 1 to 100 be simulated by setTimeout or promise.then?
Logically, if you run this program on its own outside of VUE, the output must be from 1 to 100, but why is it different in VUE?
for(let i=1; i<=100; i++){
console.log(i);
}
Copy the code
This relates to Vue’s underlying asynchronous update principle, as well as the implementation of nextTick. But before we get to nextTick, it’s worth talking about how JS events work.
JS runtime mechanism
As we all know, JS is a single-threaded language based on event loops. The steps are roughly as follows:
- When the code executes, all synchronized tasks are executed on the main thread, forming an execution stack;
- There is also a task queue outside the main thread, in which an event is placed whenever an asynchronous task has a result;
- Once all synchronization tasks in the stack have finished (the main thread code has finished executing), the main thread is not idle but reads the task queue. At this point, the asynchronous task ends and the waiting state is executed.
- The main thread repeats the above steps.
We call the main thread that executes once atick
, sonextTick
The next onetick
Use, use, usenextTick
The scene is what we want the nexttick
When you do something.
All asynchronous task results are scheduled through the task queue. Tasks fall into two categories: Macro task and micro task. The execution rule between them is that after each macro task, all microtasks are cleared. Common macro tasks are setTimeout/MessageChannel/postMessage/setImmediate, micro tasks have MutationObsever/Promise. Then.
Jake’s talk at JavaScript WORLDWIDE Developers Conference is recommended for a thorough study of event loops.
NextTick principle
Distributed update
We all know that VUE’s responsiveness relies on collecting and distributing updates. The dispatch of updates after the data is modified triggers the logic of the setter to execute dep.notify() :
// src/core/observer/watcher.js
class Dep {
notify() {
//subs is an array of Watcher instances
const subs = this.subs.slice()
for(let i=0, l=subs.length; i<l; i++){
subs[i].update()
}
}
}
Copy the code
Update (subs); update (subs); update (subs);
class Watcher {
update(){...// After all circumstances are considered
else{
queueWatcher(this)}}}Copy the code
QueueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher
/ / queueWatcher defined in SRC/core/observer/scheduler. Js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
// Repeat the optimization based on the id
if(has[id] == null){
has[id] = true
if(! flushing){ queue.push(watcher) }else{
let i=queue.length - 1
while(i > index && queue[i].id > watcher.id){
i--
}
queue.splice(i + 1.0, watcher)
}
if(! waiting){ waiting =true
FlushSchedulerQueue: Flush both queues and run the watchers
nextTick(flushSchedulerQueue)
}
}
}
Copy the code
Here the queue is optimized for pushing the watcher by ID and flushing, so instead of triggering the watcher callback every time the data changes, we add the watcher to a queue first, FlushSchedulerQueue is then executed after nextTick.
The flushSchedulerQueue function is some processing of the queue that holds update events so that updates can meet the Vue update lifecycle.
And this explains why a for loop can’t cause a page to update, because for is the main thread, and it pushes it to the queue as soon as it’s done, and by the time it’s done, I has changed to 100, Only then does vUE get to the nextTick(flushSchedulerQueue) step.
NextTick source
Then open the source of vue2.x, the directory core/util/next-tick.js, the code is very small, with comments only 110 lines, is relatively easy to understand.
const callbacks = []
let pending = false
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()
}
Copy the code
The callback cb (flushSchedulerQueue from the previous section) is first pushed into the callbacks array, and finally resolved once and for all by the timerFunc function.
let timerFunc
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () = > {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
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 (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
timerFunc = () = > {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () = > {
setTimeout(flushCallbacks, 0)}}Copy the code
TimerFunc uses the following if else feature to determine which feature to use for asynchronous tasks on different devices and in different situations: Mediate, which is a feature supported only by advanced VERSIONS of IE and Edge, does not support Promise, and does not support MutationObserver. If none of these are supported then it will be degraded to setTimeout 0.
The reason for using callbacks instead of executing callbacks directly on nextTick is to ensure that executing nextTick multiple times does not start multiple asynchronous tasks, but instead pushes them into a single synchronous task that will be executed on the nextTick.
NextTick use
NextTick is not only a source file for VUE, but also a global API for Vue. Here’s how to use it.
When vm.someData = ‘new value’ is set, the component is not immediately rerendered. When the queue is refreshed, the component is updated in the next event loop tick. In most cases we don’t need to worry about this process, but if you want to do something based on the updated DOM state, 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 have to. 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.
Official website use cases:
<div id="example">{{message}}</div>
Copy the code
var vm = new Vue({
el: '#example'.data: {
message: '123'
}
})
vm.message = 'new message' // Change the data
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
Copy the code
And because $nextTick() returns a Promise object, you can also use async/await syntax to handle events conveniently.