vue source code parsing: nextTick

Use of 1 nextTick

The image of dom in vue is not real-time. When the data changes, vue adds the rendered watcher to the asynchronous queue, asynchronous execution, and unified modification of dom after the execution of synchronous code. Let's see the following code.

<template>
  <div class="box">{{msg}}</div>
</template>

export default {
  name: 'index',
  data () {
    return {
      msg: 'hello'
    }
  },
  mounted () {
    this.msg = 'world'
    let box = document.getElementsByClassName('box')[0]
    console.log(box.innerHTML) // hello
  }
}

As you can see, dom is not updated immediately after data modification. dom updates are asynchronous and cannot be retrieved by synchronous code. nextTick is needed to be retrieved in the next event loop.

this.msg = 'world'
let box = document.getElementsByClassName('box')[0]
this.$nextTick(() => {
  console.log(box.innerHTML) // world
})

If we need to get the updated dom information, such as dynamic acquisition of width and height, location information, etc., we need to use nextTick.

Principle Analysis of dom Update and nextTick for Data Change

2.1 Data changes

vue bidirectional data binding relies on ES5's Object.defineProperty. When data is initialized, getter and setter are created for each attribute through Object.defineProperty, which turns data into responsive data. When modifying attribute values, such as this.msg = world, setter is actually triggered. Look at the source code below, in order to facilitate the more read, the source code has been deleted.

Data change trigger set function

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  // Triggering set function after data modification completes dom update through a series of operations
  set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    if (getter && !setter) return
    if (setter) {
      setter.call(obj, newVal)
    } else {
      val = newVal
    }
    childOb = !shallow && observe(newVal)
    dep.notify() // Execute dep notify method
  }
})

Execute dep.notify method

export default class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      // In fact, traversal executes the update method for elements in subs arrays
      subs[i].update()
    }
  }
}

When data is referenced, such as <div>{msg} </div>, get method is executed, rendering Watcher is added to subs array, and updating method of Watcher is executed when data is changed.

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this) //Execute queueWatcher
  }
}

The update method finally executes queueWatcher

function 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) {
      // Ensure that nextTick is executed only once through waiting
      waiting = true
      // The queueWatcher method eventually passes the flush Scheduler Queue into nextTick for execution.
      nextTick(flushSchedulerQueue)
    }
  }
}

Execute the flush SchedulerQueue method

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  ...
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // Traversing the run method of rendering watcher to complete view updating
    watcher.run()
  }
  // Reset waiting variable 
  resetSchedulerState()
  ...
}

That is to say, when data changes eventually pass flush Scheduler Queue into nextTick to execute the flush Scheduler Queue function, the watcher.run() method will traverse the execution of the watcher.run() method, and the watcher.run() method will eventually complete the view update. Next, let's see what the key nextTick method is.

2.2 nextTick

The nextTick method is push ed into the callbacks array by the incoming callback, and then the timerFunc method is executed.

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // push into callbacks array
  callbacks.push(() => {
     cb.call(ctx)
  })
  if (!pending) {
    pending = true
    // Execute the timerFunc method
    timerFunc()
  }
}

timerFunc

let timerFunc
// Determine whether native support for Promise is available
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    // If native support for Promise implements flush Callbacks with Promise
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
// Determine whether native support for Mutation Observer is available
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  // If native support for Mutation Observer implements flush Callbacks with Mutation Observer
  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
// Determine whether setImmediate is naturally supported 
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
  // If setImmediate is natively supported, flush Callbacks are executed with setImmediate
    setImmediate(flushCallbacks)
  }
// Use setTimeout 0 without support
} else {
  timerFunc = () => {
    // Using setTimeout to execute flush Callbacks
    setTimeout(flushCallbacks, 0)
  }
}

// flushCallbacks ultimately executes the callback function passed in by the nextTick method
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

nextTick will use microTask first, followed by macroTask.

That is to say, tasks in nextTick actually execute asynchronously, and nextTick(callback) is similar to
Promise.resolve().then(callback), or setTimeout(callback, 0).

That is to say, vue's view update nextTick (flush Scheduler Queue) is equivalent to setTimeout (flush Scheduler Queue, 0), which asynchronously executes the flush Scheduler Queue function, so we do not update dom immediately at this.msg = hello.

To read the dom information after the dom update, we need to create an asynchronous task after the asynchronous task is created.

To validate this idea, we don't need nextTick to experiment with setTimeout directly. The following code validates our idea.

<template>
  <div class="box">{{msg}}</div>
</template>

<script>
export default {
  name: 'index',
  data () {
    return {
      msg: 'hello'
    }
  },
  mounted () {
    this.msg = 'world'
    let box = document.getElementsByClassName('box')[0]
    setTimeout(() => {
      console.log(box.innerHTML) // world
    })
  }
}

If we nextTick before data modification, the asynchronous tasks we add will be executed before the rendered asynchronous tasks, and the updated dom will not be available.

<template>
  <div class="box">{{msg}}</div>
</template>

<script>
export default {
  name: 'index',
  data () {
    return {
      msg: 'hello'
    }
  },
  mounted () {
    this.$nextTick(() => {
      console.log(box.innerHTML) // hello
    })
    this.msg = 'world'
    let box = document.getElementsByClassName('box')[0]
  }
}

3 Summary

In order to ensure performance, vue adds DOM modifications to asynchronous tasks. After all synchronous codes are executed, DOM modifications are unified. Multiple data modifications in an event cycle only trigger watcher.run(). That is, through nextTick, nextTick gives priority to creating asynchronous tasks using microTask. If you need to get the modified DOM information in the vue project, you need to create an asynchronous task after the DOM update task through nextTick. As stated on the official website, nextTick will perform a delayed callback after the next DOM update cycle ends.

Reference Articles

Vue nextTick mechanism

ps: Welcome to pay attention to the Wechat Public Number, Front-end Roaming Guide, which has just been launched. It will regularly publish high-quality original articles and translations. Thank you.~

Tags: Javascript Vue Attribute iOS

Posted on Mon, 12 Aug 2019 06:12:40 -0700 by trystan