background

Let’s first look at the 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)
    }
  }
}
Copy the code

The script execution, we assume, prints in sequence: 1, 2, 3. In practice, however, the output is only once: 3. Why does this happen? Let’s find out.

queueWatcher

$watch(keyOrFn, handler, options) is actually called by Vue. $watch is a function that we bind to the VM when we initialize it to create a Watcher object. So let’s look at how 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)}}...Copy the code

Initialize this.deep = this.user = this.lazy = this.sync = false, that is, when the update is triggered, the queueWatcher method is executed:

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false. exportfunction 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)
    }
  }
}
Copy the code

NextTick (flushSchedulerQueue) flushSchedulerQueue (flushSchedulerQueue)

function flushSchedulerQueue () {
  flushing = true
  let watcher, id
  ...
 for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = nullwatcher.run() ... }}Copy the code

In addition, with regard to the waiting variable, this is an important flag that ensures that the flushSchedulerQueue callback is only allowed to be inserted into Callbacks once. Next, let’s look at the nextTick function. Before nexTick, you need to have a certain understanding of Event Loop, microTask, macroTask, Vue nextTick is mainly used in these basic principles. If you don’t already know about the Event Loop, you can refer to my article introduction to the Event Loop. Here’s 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 (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
    timerFunc = (a)= > {
      setImmediate(nextTickHandler)
    }
  } else if (typeofMessageChannel ! = ='undefined' && (
    isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]'
  )) {
    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = nextTickHandler
    timerFunc = (a)= > {
      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 = (a)= > {
      p.then(nextTickHandler)
    }
  } else {
    // fallback to setTimeout
    timerFunc = (a)= > {
      setTimeout(nextTickHandler, 0)}}return function queueNextTick (cb? : Function, ctx? : Object) {
    let _resolve
    callbacks.push((a)= > {
      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
      })
    }
  }
})()
Copy the code

First, Vue simulates the event queue through the callback array. The events in the queue are called through the nextTickHandler method, and timerFunc decides what to do. Let’s look at the definition of timeFunc:

  if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
    timerFunc = (a)= > {
      setImmediate(nextTickHandler)
    }
  } else if (typeofMessageChannel ! = ='undefined' && (
    isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]'
  )) {
    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = nextTickHandler
    timerFunc = (a)= > {
      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 = (a)= > {
      p.then(nextTickHandler)
    }
  } else {
    // fallback to setTimeout
    timerFunc = (a)= > {
      setTimeout(nextTickHandler, 0)}}Copy the code

TimerFunc defines macroTask > microTask. In a Dom – free environment, microTask is used, such as Weex

SetImmediate, MessageChannel VS setTimeout

We’re defining setImmediate and MessageChannel first why do we create macrotasks with them first instead of setTimeout? HTML5 stipulates that the minimum time delay for setTimeout is 4ms, which means that in an ideal environment, the asynchronous callback can be triggered only after 4ms. Vue uses so many functions to simulate asynchronous tasks with the sole purpose of making callbacks asynchronous and called as early as possible. MessageChannel and setImmediate have significantly less latency than setTimeout.

To solve the problem

With that in mind, let’s look at the above questions again. Because the event mechanism of Vue is to schedule execution through the event queue, the main process will be scheduled after the execution is idle, so go back to wait for all processes to complete the execution and then update. The performance benefits are obvious, such as:

There is a case where the value of test is mounted and executed 1000 times in the ++ loop. Setter ->Dep->Watcher->update->run If the view is not updated asynchronously at this point, then each ++ will directly manipulate the DOM to update the view, which is very performance consuming. So Vue implements a queue, and the next Tick (or microtask phase of the current Tick) runs the Watcher in the queue. In addition, Watcher with the same ID will not be added to the queue repeatedly, so the Watcher run will not be executed 1000 times. The final update to the view will simply directly change the DOM for test from 0 to 1000. Greatly optimizes performance by ensuring that the update view action DOM is called on the next Tick (or microtask phase of the current Tick) after the current stack finishes executing.

Interesting question

var vm = new Vue({
    el: '#example'.data: {
        msg: 'begin',
    },
    mounted () {
      this.msg = 'end'
      console.log('1')
      setTimeout((a)= > { // macroTask
         console.log('3')},0)
      Promise.resolve().then(function () { //microTask
        console.log('promise! ')})this.$nextTick(function () {
        console.log('2')})}})Copy the code

The execution order of this must be known as: 1, promise, 2, 3.

  1. MSG = ‘end’, which causes watcher’s update to be triggered, and thus pushes callback push into vUE’s event queue.

  2. This.$nextTick also pushes into a new callback function for the event queue that defines timeFunc through setImmediate –> MessageChannel –> Promise –> setTimeout. Promise.resolve().then is a microTask, so the Promise will be printed first.

  3. In the case of MessageChannel and setImmediate, their execution order is prioritized over setTimeout (in IE11/Edge, setImmediate latency can be within 1ms, and setTimeout has a minimum latency of 4ms, So setImmediate performs callbacks earlier than setTimeout(0). Second, because the event queue, the revenue callback array is preferred), 2 is printed, followed by 3

  4. But in the absence of MessageChannel and setImmediate support, timeFunc is defined through promises, and previous versions prior to Vue 2.4 execute promises first. This causes the order to become: 1, 2, PROMISE, 3. Promise.resolve().then(function () {console.log(‘ Promise! ‘)}), and then define $nextTick to be stored in the callback. We know that the queue satisfies the first-in, first-out principle, so the object that the callback receives will be executed first.

Afterword.

If you are interested in the Vue source code, you can come here:

More fun Vue convention source explanation

Reference article:

Vue.js upgrade step pit tips

Vue DOM asynchronous update strategy and nextTick mechanism