The problem

<div id="app">
    <p ref="ref" v-if="isShow">{{ message }}</p>
    <button @click="getWidth">Gets the p element width</button>
</div>

<script>
    export default {
        data() {
            return {
                isShow: false.message: 0}},methods: {
            getWidth() {
                this.isShow = isShow;
                this.message = this.$ref.ref.offsetWidth; }}}</script>
Copy the code

Let’s see what’s wrong with the code above, which displays the P tag when the button is clicked and then gets the width to display on the page. In fact, this.$ref.ref is not available immediately after the assignment, so clicking on the button will result in an error.

why

Vue is executed asynchronously when updating the DOM. As long as it listens for data changes, Vue opens a queue and buffers all data changes that occur in the same event loop. In the above code, it is not possible to fetch the latest DOM node immediately after updating the data, since it has not been updated to the DOM structure.

The solution

Vue provides a nextTick method that is used immediately after the data is modified to retrieve the updated DOM.

Official explanation:

Vue.nextTick( [callback, context] )

  • Parameters:

    • {Function} [callback]
    • {Object} [context]
  • Usage:

    A deferred callback is performed after the next DOM update loop ends. Use this method immediately after modifying the data to get the updated DOM.

    // Modify the data
    vm.msg = 'Hello'
    // DOM has not been updated yet
    Vue.nextTick(function () {
      // DOM is updated
    })
    
    // Use as a Promise (new since 2.1.0, see hints below)
    Vue.nextTick()
      .then(function () {
        // DOM is updated
      })
    Copy the code

So let’s modify the previous code as follows

getWidth() {
    this.isShow = isShow;
    this.nextTick(() = > {
        this.message = this.$ref.ref.offsetWidth; })}Copy the code

This will get the updated DOM.

NextTick source

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()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

The above code is the implementation of nextTick in Vue. Let’s take a look at the code.

He’s going to put that callback method in the callbacks array, and then call the timerFunc method, so let’s look at the timerFunc function

if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () = > {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}Copy the code

FlushCallbacks: flushCallbacks: flushCallbacks: flushCallbacks: flushCallbacks: flushCallbacks: flushCallbacks: flushCallbacks

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
Copy the code

This function copies a callbacks array and executes the callbacks in turn.