vue View Renewal Principle and nextTick() Principle

Posted by pgsjoe on Tue, 20 Aug 2019 08:32:38 +0200

vue View Updating Principle

  1. Tracking changes
    When you pass a normal JavaScript object into a Vue instance as a data option, Vue will traverse all the attributes of the object and use Object.defineProperty to convert all these attributes to getter/setter.
  2. Asynchronous update queue
    Vue updates DOM asynchronously. As long as data changes are detected, Vue opens a queue and buffers all data changes that occur in the same event cycle. If the same watcher is triggered multiple times, it will only be pushed into the queue once. This removal of duplicate data during buffering is important to avoid unnecessary computations and DOM operations. 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, Mutation Observer and setImmediate for asynchronous queues. If the execution environment does not support it, setTimeout(fn, 0) will be used instead.

For example, when you set vm. someData ='New value', the component will not be re-rendered immediately. When the queue is refreshed, the component is updated in the next event loop, tick. In most cases, we don't need to care about this process, but if you want to do something based on the updated DOM status, it may be a bit tricky. Although Vue.js usually encourages developers to think in a "data-driven" way and avoid direct exposure to DOM, sometimes we have to do so. In order to wait for Vue to update DOM after data changes, you can use Vue.nextTick(callback) immediately after data changes. This callback function will be called after the DOM update is completed. For example:

var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // change data
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

The above is the official document explanation. I understand as follows
When the data changes, vue will open an asynchronous queue, which includes Promise.then, Mutation Observer, setImmediate, setTimeout(fn, 0). vue will automatically determine which asynchronous events are supported by the current environment and use the corresponding asynchronous api in turn.

Event loop: It is divided into micro task queue and macro task queue.
Microtask queues: Promise, process. nextTick, Promises, Object. observe, Mutation Observer
Macro task queue: script (whole code), setTimeout, setInterval, setImmediate, I/O, UI rendering

this.msgtest = '222'
this.$nextTick(() => {
    console.log(this.$refs.msg.innerText)
})
this.msg = 'xinde'

As shown in the above code: When the value of this.msgtest is changed, an asynchronous queue (Promise) will be opened. Changing this.msg again in the same event loop will not open the asynchronous queue, but will put the callback of msg into the asynchronous queue just opened. In the above code, the asynchronous queue is opened first, and the nextTick event is added. NextTick event also opens the asynchronous queue, so the asynchronous queue of nextTick will be executed later, and the latest value of msg will be obtained at this time.

this.$nextTick(() => {
   console.log(this.$refs.msg.innerText)
})
this.msg = 'xinde'

Look at this code: at this point, first execute nextTick to open the asynchronous queue tick1, then execute this.msg to open the update dom asynchronous queue tick2, tick1 registered before tick2, tick1 will execute first, so the latest value of MSG can not be found in the callback function of nextTick. This requires us to place the next Tick after all the data operations to ensure the execution order of the asynchronous queues.

nextTick() Principle

Source code

/**
 * Defer a task to execute it asynchronously.
 */
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]()
    }
  }

  // the nextTick behavior leverages the microtask queue, which can be accessed
  // via either native Promise.then or MutationObserver.
  // MutationObserver has wider support, however it is seriously bugged in
  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  // completely stops working after triggering a few times... so, if native
  // Promise is available, we will use it:
  /* istanbul ignore if */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // 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)
    }
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (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()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()

Simple explanation: The nextTick function is a self-running function that returns directly to the queueNextTick method, which pushes the incoming callback function into callbacks and then executes timerFunc(). timerFunc() can open an asynchronous queue, and the callback function of the asynchronous queue is nextTickHandler, which will execute all the callback functions in the callbacks. The key is the asynchronous queue opened by tiemrFunc, and the callback function in the nextTick will be executed after the asynchronous event call. In conjunction with the previous section, you can get the latest dom values

Topics: Front-end Vue iOS Javascript Android