Before we get into nextTick, you should know that Vue updates the DOM asynchronously, because we’re going to be talking about component updates. Let’s get straight to the point (this article is explained with Vue source code version v2.6.14)

1. NextTick Quiz

Do you really know nextTick? Let’s get right to the problem

<template>
  <div id="app">
    <p ref="name">{{ name }}</p>
    <button @click="handleClick">Modify the name</button>
  </div>
</template>

<script>
  export default {
  name: 'App',
  data () {
    return {
      name: 'Well Boran'}},mounted() {
    console.log('mounted'.this.$refs.name.innerText)
  },
  methods: {
    handleClick () {
      this.$nextTick(() = > console.log('nextTick1'.this.$refs.name.innerText))
      this.name = 'jngboran'
      console.log('sync log'.this.$refs.name.innerText)
      this.$nextTick(() = > console.log('nextTick2'.this.$refs.name.innerText))
    }
  }
}
</script>
Copy the code

$refs.name.innerText = ‘nextTick1’, ‘sync log’, ‘nextTick2’ innerText = ‘refs.name.innerText’ Notice that we print the innerText of the DOM (the answer is posted at the end of the article).

If you have a very firm answer at this point, then you don’t have to move on. But if you have any doubts about your answer, follow me and move on. Believe that you read, do not need to see the answer can have a positive answer ~!


Second, nextTick source code implementation

The source code is in core/util/next-tick. You can break it down into four parts and go straight to the code

1. Global variables

Callbacks queue and pending status

const callbacks = [] // Queue for storing CB
let pending = false // Whether to immediately traverse the queue and execute the CB flag
Copy the code

2. flushCallbacks

Iterate through the callbacks to execute each CB

function flushCallbacks () {
  pending = false // Notice that pending is reset to false immediately after execution
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // Execute each cb}}Copy the code

3. nextTicktheasynchronousimplementation

Different asynchronous implementation strategies are adopted depending on the support of the execution environment

let timerFunc // nextTick implements fn asynchronously

if (typeof Promise! = ='undefined' && isNative(Promise)) {
  / / Promise
  const p = Promise.resolve()
  timerFunc = () = > {
    p.then(flushCallbacks) // wrap flushCallbacks into promise.then
  }
  isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  / / MutationObserver scheme
  let counter = 1
  const observer = new MutationObserver(flushCallbacks) // use flushCallbacks as the cb to observe changes
  const textNode = document.createTextNode(String(counter)) // Create a text node
  // Observe the text node changes
  observer.observe(textNode, {
    characterData: true
  })
  // timerFunc changes the text node's data to trigger the observed callback flushCallbacks
  timerFunc = () = > { 
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  / / setImmediate scheme
  timerFunc = () = > {
    setImmediate(flushCallbacks)
  }
} else {
  // The final demote scheme setTimeout
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}Copy the code
  • Here’s a real case to reinforce the pointMutationObserverUnderstanding. After all, compared to the other three asynchronous solutions, this should be the most unfamiliar
    const observer = new MutationObserver(() = > console.log('Text node change observed'))
    const textNode = document.createTextNode(String(1))
    observer.observe(textNode, {
        characterData: true
    })
    
    console.log('script start')
    setTimeout(() = > console.log('timeout1'))
    textNode.data = String(2) // Change the value of the text node
    console.log('script end')
    Copy the code
  • Do you know what the corresponding output would look like?
    1. script start,script endWill be executed in the first round of macro tasks, that’s fine
    2. setTimeoutWill be put into the next macro task execution
    3. MutationObserverIs a microtask, so it will be executed after this macro task, so it precedessetTimeout
  • The results are shown below:


4. nextTickMethod implementation

Cb, Promise

export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  // Add cb to the global Callbacks queue
  callbacks.push(() = > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      // Here is a way to support Promise
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    // Execute timerFunc, and execute all cb callbacks in the next Tick
    timerFunc()
  }
  // Implementation of Promise, which is why we can write nexttick. then
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code
  • Go into detail, understandpendingWhat does it do? How does it work?
  1. In case 1, execute the Tick twice in the same round$nextTick.timerFuncIt will only be executed once
this.$nextTick(() = > console.log('nextTick1'))
this.$nextTick(() = > console.log('nextTick2'))
Copy the code
  • It’s a little bit more intuitive, right?


Asynchronous update of Vue components

Here if there is a Vue componentization, distribution update is not very understanding of friends, you can first stamp here, see the diagram of Vue response principle to understand the Vue componentization and distribution update related content and then come back to see oh ~

Vue’s asynchronous DOM update is actually implemented using nextTick, which is the same as the usual $nextTick

So just to review, what happens when we change the value of an attribute?

We start with watcher.update when it startsRender the WatcherFor example, enterqueueWatcherIn the

1. queueWatcherWhat did you do?

// A queue for Wathcer. Note that this is not to be confused with the nextTick callbacks, they are all queues, but they do different things
const queue: Array<Watcher> = []

function queueWatcher (watcher: Watcher) {
  const id = watcher.id // Get the Wathcer ID, which is globally unique to each watcher
  if (has[id] == null) {
    // Avoid adding duplicate wathcers, which is also an optimization for asynchronous rendering
    has[id] = true
    if(! flushing) { queue.push(watcher) }if(! waiting) { waiting =true
      FlushSchedulerQueue is pushed into the nextTick callbacks queue
      nextTick(flushSchedulerQueue)
    }
  }
}
Copy the code

2. flushSchedulerQueueWhat did you do?

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  // Sort to ensure that the parent performs the update after the child, and ensure that the userWatcher is rendered before the Watcher
  queue.sort((a, b) = > a.id - b.id)
  // Run through all the Watcher that needs to be updated
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    Render -> update -> patch
    watcher.run()
  }
}
Copy the code
  • Finally, a diagram illustrates the component’s asynchronous update process


Fourth, return to the topic itself

We believe that the above analysis of nextTick’s source code has uncovered its mystery. At this time you can certainly firmly say the answer ~ words don’t say much, we check together, see if it is as you think!

  1. As the picture shows,mountedThe innerText of time is the Chinese word for “Jing Boran.

  2. The next step is to click the button and print the result as shown in the figure

  • Yes, the output is as follows (surprise? Is it a surprise?)

    1. Sync log Well Boran
    2. NextTick1 jingboran
    3. nextTick2 jngboran
  • Here’s a quick look at each output:

    this.$nextTick(() = > console.log('nextTick1'.this.$refs.name.innerText))
    this.name = 'jngboran'
    console.log('sync log'.this.$refs.name.innerText)
    this.$nextTick(() = > console.log('nextTick2'.this.$refs.name.innerText))
    Copy the code
    1. sync log: This synchronous printing nothing to say, I believe that most children’s shoes are not the question point here. Take a look back at EventLoop if you don’t know
    2. nextTick1: Note that it is placed$nextTickThe callback in the next tick executes, but hislocationIs in thethis.name = 'jngboran'The former. In other words, his CB willthanApp Component distribution updates (flushSchedulerQueue)More into the firstQueue, whennextTick1When you print, the App component hasn’t sent out the update yet, so you get the old DOM value.
    3. nextTick2I’m not going to expand it, but you can analyze it for yourself. I believe you should be the most positive about it, we usually don’t get the updated DOM in this way?
  • Here’s a final picture to help you understand


At the end, nextTick is actually a core part of Vue. Because of the componentized, responsive distribution of updates throughout the Vue application, an in-depth understanding of the implementation principles behind nextTick will not only help you in your job interview, but also help you avoid pitfalls in your daily development work. Well, this article is over here, if you can read it, please give me a thumbs up. Drawing is not easy, creating is hard