Disclaims: All source code in this article is taken from Vue on the dev branch of Version: 2.5.13. The absolute accuracy of the opinions in this article is not guaranteed. The article was compiled from my internal sharing in the group this week.

Original address of the article

Our current technology stack is mainly Vue, and we encountered a situation where we needed to reset the lifecycle of the entire component when the props passed in some component were changed (for example, changing the type of the datepicker in the IView, The good news is that the component no longer has to use such a silly method to switch the type of time display. To do this, we have the following code

<template>
  <button @click="handleClick">btn</button>
  <someComponent  v-if="show" />
</template>

<script>
  {
    data() {
      return { show: true }
    },
    methods: {
      handleClick() {
        this.show = false
        this.show = true
      }
    }
  }
</script>
Copy the code

Don’t laugh, of course we know how stupid this code is. We don’t have to try to make sure it’s wrong, but with react experience I probably know how to replace this.show = true with setTimeout(() => {this.show = true}, 0), You should get the desired result, and sure enough, the component resets its life cycle, but things are still a little off. After a few clicks, the component always flashes. Logically this makes sense, it’s normal for components to be destroyed and then rebuilt, but sorry, we found another way (Google does everything, after all), Set setTimeout(() => {this.show = true}, 0) to this.$nextTick(() => {this.show = true}). But the components didn’t flash at all.

In order to make my dear you feel my illusory description, I prepared this demo for you. You can change handle1 into Handle2 and Handle3 in turn to experience the pleasure of components wandering between flashing and not flashing.

If you’re still reading after the fun is over, I’ll tell you this is going to take a long time, because to fully understand this we need to get inside the Vue and Javascript EventLoop.

The main reason for this problem is that Vue adopts the asynchronous update queue mode by default. We can find the following description from the official website

In case you haven’t noticed, 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.

This does give a concise description of the process, but it doesn’t solve our puzzle. It’s time to show the real technology. It is important to note that the following core process if you have not read some of the source blogs or read the source code, you may be confused. But it doesn’t matter what we care about in the end here is basically step 4, you just need to memorize it roughly, and then map the process to the source code that we will parse later.

The core process of Vue can be broken down into the following steps

  1. The traversal property adds get, set methods that collect dependencies (dev.subs.push(watcher)), and set methods that call notify of dev, This method notifies all the Watchers in the subs and calls the Update method of the Watcher, which we can think of as publish and subscribe in design mode

  2. By default, the update method triggers queueWatcher, which adds the watcher instance itself to a queue (queue.push(watcher)) and then calls nextTick(flushSchedulerQueue).

  3. FlushSchedulerQueue is a flushSchedulerQueue function that calls the watcher.run methods of all the watcher’s in the queue

  4. FlushSchedulerQueue doesn’t execute at this point. All we do in step 2 is put the flushSchedulerQueue into a callbacks queue (callbacks.push(flushSchedulerQueue)). The callbacks are then iterated through and executed asynchronously (this is an asynchronous update queue)

  5. The flushSchedulerQueue is called watcher.run() when it is executed, and you see a new page

All of the above flows are in the vue/ SRC /core folder.

Let’s look at the execution of the Vue code in the last case in the example above. I’ll omit some of the details, but keep in mind that the only thing we care about here is the fourth step

When the button is clicked, the callback bound to the button is fired, this.show = false is executed, and the set function in the property is triggered. In the set function, dev’s notify method is called, Cause each of the Watcher’s update methods in its subs to be executed (in this case there is only one watcher~ in the subs array). Let’s look at the constructor of the watcher

class Watcher {
  constructor (vm) {
    // Bind the Vue instance to the VM properties of Watcher
    this.vm = vm 
  }
  update () {
     // By default, it goes to the else branch and calls Watcher's run method directly
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}}Copy the code

Take a look at queueWatcher

/** * Push the watcher instance to queue(an array). * The watcher marked by has object is not added to queue again */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // Check whether watcher is marked. Has is an object
  if (has[id] == null) {
    // The unmarked watcher enters the branch and is marked
    has[id] = true
    if(! flushing) {// Push to queue
      queue.push(watcher)
    } else {
      // If it was added to the flush queue, it will be inserted in the correct position according to its Watcher ID
      // If unfortunately the watcher has missed the time to be called, it will be called immediately
      FlushSchedulerQueue will understand the meaning of these two comments later
      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
     // The nextTick function that we're interested in is called this.$nextTick
      nextTick(flushSchedulerQueue)
    }
  }
}
Copy the code

When this function runs, our watcher is in the queue (in this case, only one watcher is added to the queue), and nextTick(flushSchedulerQueue) is called, FlushSchedulerQueue = flushSchedulerQueue = flushSchedulerQueue

/** * flush the entire queue, call watcher */
function flushSchedulerQueue () {
  // Set flush to true, see above
  flushing = true
  let watcher, id

  // Flush the queue before sorting
  / / the purpose is
  // 1. The creation and updating of components in Vue is similar to event capture in that it extends from the outermost layer to the inner layer, so first
  // Call the creation and update of the parent component
  // 2. UserWatcher was created earlier than renderWatcher.
  // 3. If the parent component's watcher calls run and the parent component is dead, then the child component's watcher does not need to be called
  queue.sort((a, b) = > a.id - b.id)
  
  // The length of the queue is not cached here, since the queue may still be added during the loop
  for (index = 0; index < queue.length; index++) {
    // Fetch each watcher
    watcher = queue[index]
    id = watcher.id
    // Clear the mark
    has[id] = null
    // Update dom to go
    watcher.run()
    // Check whether an infinite loop exists in the dev environment
    if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break}}}Copy the code

Remember that at this point our flushSchedulerQueue is not yet executed, it is simply passed into nextTick as a callback. Next we will focus on nextTick and recommend that you take a look at the source code for nextTick as a whole, although I will explain that as well

We’ll start by extracting the withMacroTask function from next-tick.js. I’m sorry I left this function to the end, because I want you to know, dear, that the most important thing always comes last. But in the overall process when we click on BTN, the first step should actually be to call this function.

/** * wrap the fn argument to use marcoTask * where fn is the callback function we bind to the event */
export function withMacroTask (fn: Function) :Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null.arguments)
    useMacroTask = false
    return res
  })
}
Copy the code

Yes, the callback you bind to onclick is triggered by apply within this function, so make a breakpoint here to verify that. Ok, now I’m sure you’ve proved that to me, but it doesn’t really matter, because the important thing is that we have a flag here, useMacroTask = true, that’s the key thing, Google translate and we can see what that means, using macro tasks

OK, that starts with the EventLoop, part two, at the beginning of this article.

In fact, I believe that this part of the content has already seen here for you to have contacted, if it is really not clear, recommend you carefully look at ruan a teacher’s article, we will only make a summary

  1. Our synchronization task calls form a stack structure
  2. In addition, we have a task queue. When an asynchronous task has a result, one task is added to the queue, and each task corresponds to a callback function
  3. When our stack structure is empty, the task queue is read and its corresponding callback function is called
  4. repeat

What is lacking from this summary is that the tasks in the queue are actually divided into two types, macrotasks and microtasks. When all synchronization tasks on the main thread have finished, all microtasks are extracted from the task queue, and when the microtasks have also finished, the event loop ends and the browser rerenders (keep this in mind, as this is the reason for the problem described at the beginning of this article). After that, the macro task is removed from the queue to continue the next round of the event cycle. It is worth noting that the micro task can still continue to be generated during the execution of the micro task in this round of the event cycle. So microtasks are essentially prioritized over macro tasks.

If you want to learn more about macro versus micro tasks, I recommend reading this article, which is probably the best, most understandable, and detailed article on this subject in the Eastern hemisphere.

The macro task is not generated in the same way as the microtask. SetImmediate, MessageChannel, and setTimeout generate the macro task in the browser environment, while MutationObserver and Promise generate the microtask. This is what Vue does asynchronously. Vue uses the Boolean value of useMacroTask to determine whether to create a macro task or a microtask to update the queue asynchronously. We’ll see this later, but let’s go back to our original logic.

When fn is called within the withMacroTask function all of the steps we’ve described above are generated. Now it’s time to really see what the nextTick function does

export function nextTick (cb? : Function, ctx? : Object) {
  let _resolve
  // Callbacks is an array that pushes cb into the array. In this case, this CB is the flushSchedulerQueue that has not yet been executed
  callbacks.push((a)= > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  // flag the bit to ensure that the following code will not be executed again if operations such as this.$nextTick occur later
  if(! pending) { pending =true
    // Use microtask or macro task. In this example, the Vue has chosen macro task so far
    // All data changes directly generated by v-ON binding events are macro tasks
    // Since our bound callbacks are wrapped with withMacroTask, withMacroTask sets useMacroTask to true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

After executing the above code, there are only two results left: macroTimerFunc or microTimerFunc, which in this case is macroTimerFunc. The purpose of both of these functions is to iterate through callbacks asynchronously, but as we mentioned above, they do it in different ways, one for macro tasks and the other for micro tasks. This.$nextTick(() => {this.show = true})) has not yet been executed, but don’t despair, it will soon be executed. Okay, back to macroTimerFunc and microTimerFunc.

/** * macroTimerFunc */
// If your environment supports setImmediate, use it to generate macro tasks for asynchronous effects
if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  macroTimerFunc = (a)= > {
    setImmediate(flushCallbacks)
  }
} else if (typeofMessageChannel ! = ='undefined' && (
  / / otherwise MessageChannel
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = (a)= > {
    port.postMessage(1)}}else {
  // You can only use setTimeout
  /* istanbul ignore next */
  macroTimerFunc = (a)= > {
    setTimeout(flushCallbacks, 0)}}Copy the code
/** * microTimerFunc */
// Use promises to generate microtasks if promises are supported
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = (a)= > {
    p.then(flushCallbacks)
    // Do compatibility processing for IOS, (there are some problems in IOS, please see your own explanation)
    if (isIOS) setTimeout(noop)
  }
} else {
  / / demote
  microTimerFunc = macroTimerFunc
}
Copy the code

In fact, the final effect that nextTick hopes to achieve is to call flushCallbacks asynchronously. As for whether to use macro task or micro task, Vue has already handled it for us and we don’t have to decide. In the case of flushCallbacks, just look at the name of the flushCallbacks.

function flushCallbacks () {
  pending = false
  // Make a copy of the callbacks
  const copies = callbacks.slice(0)
 / / empty callbacks
  callbacks.length = 0
  // Traverse and execute
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
Copy the code

Remember that the flushCallbacks are handled asynchronously, and the current synchronization task has not yet been completed, so this function is not called at this point. What we really need to do is to complete the synchronization task. $nextTick(() => {this.show = true}); When this.$nextTick is called () => {this.show = true} is also pushed into the callbacks as an argument, [flushSchedulerQueue, () => {this.show = true}] The synchronization task is complete.

Now remember in eventLoop, we look for all the microtasks in the task queue, and so far there aren’t any microtasks in the task queue, so the loop completes, the browser rerenders, but our DOM structure doesn’t change at all, So it doesn’t matter if the browser doesn’t rerender. Now it’s time to execute the macro task in the task queue, whose corresponding callback is the flushCallbacks we just registered. In flushSchedulerQueue, the watcher is called “run”. Since show in our data is changed to false, we remove the components bound to V-if =”show” from the real DOM after comparing the old and new virtual dom.

The important point is that although this component is removed from the DOM, it is still visible in the browser because our event loop is not complete, there are still synchronization tasks that need to be performed, and the browser has not started redrawing. (If you have any questions about this, I personally think you may not understand the difference between the DOM and the browser. You can interpret the DOM as all the nodes in the Elements module of the console, and the browser does not display the same content at all times.)

All that remains to be done is () => {this.show = true}, and when this.show = true all of the above procedures are executed again, with only a few details that are different, let’s take a look.

  1. This function is not wrapped with withMacroTask, it is called when the callbacks are flushed, so the useMacrotask has not been changed and its default value is false

  2. For the first reason, the microtask is generated to handle the flushCallbacks in flushCallbacks when we execute the macroTask.

So when macroTask ends, the event loop is not finished, and we still have microtasks to handle. We still call flushSchedulerQueue, and then watcher.run. Rebuild the component and the lifecycle completes the reset. At this point, the event loop ends and the browser rerenders. Hopefully, you will remember that our browser itself is now in a state where the component is visible, and it will still be visible after rerendering, so naturally there will be no flashing of the component.

Now I’m sure you can figure out for yourself why setTimeout is flashing in our example, but let me give you a reason to see if you agree with me. Because setTimeout generates macro tasks, when the event loop completes, the macro tasks are not handled directly, and browser drawing is inserted in the middle. After the browser redraws, it removes the displayed component, so the area is blank, and then the next event loop begins, the macro task is executed and the component DOM is recreated, the event loop ends, the browser redraws, and the component is displayed again in the visible area. So in your visuals, the component will flash, and the whole process will be over.

At last we have said all we want to say, if you can persist in watching here, thank you very much. But there are still a few things we need to consider.

  1. Why would Vue use an asynchronous queue update? It’s a hell of a hassle

Well, the documentation already tells us that

This removal of duplicate data while buffering is important to avoid unnecessary computation and DOM manipulation.

Let’s assume that the flushSchedulerQueue is not called through nextTick, so the first way to write it is this.show = false; This. Show = true triggers the watcher.run method, which can also reset the component’s life cycle, You can comment out nextTick(flushSchedulerQueue) in the Vue source code and use the flushSchedulerQueue() interrupt point to experience the process more explicitly. Keep in mind that this is just a simple example of how many times the DOM has been changed for nothing because of this problem. We all know that DOM manipulation is expensive, so Vue helped us optimize this step within the framework. Consider also flushSchedulerQueue() to see if the component flashes to consolidate what we just said.

  1. Since the microtask used by nextTick is generated by promise.then ().resolve(), can we write it directly in the callback functionthis.show = false; Promise.then().resolve(() => { this.show = true })Instead of this.$nextTick? Obviously it’s not a good idea to ask, but you need to think about the process yourself.

Finally, thanks for reading ~~~