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:

  1. When the code executes, all synchronized tasks are executed on the main thread, forming an execution stack;
  2. There is also a task queue outside the main thread, in which an event is placed whenever an asynchronous task has a result;
  3. 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.
  4. The main thread repeats the above steps.

We call the main thread that executes once atick, sonextTickThe next onetickUse, use, usenextTickThe scene is what we want the nexttickWhen 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.