[Vue] Explore nextTick

This article is based on VUE 2.6.11 source code. The implementation of Vue 3.0 has changed slightly, but the ideas can still be borrowed from 2.6. The 3.0 changes will be covered at the end of the article.

I’ve had a lot of questions about nextTick:

  1. How does this function perform a callback after a DOM update?

  2. In reactive data modification, there is a phenomenon that all modifications are merged and unified. How to achieve this phenomenon?

    We have the response data a, b, a is equal to'to modify a'; NextTick (print dom corresponding to a and B) b ='modify b';// The control console prints a and b corresponding to the DOM are modified, how to do this?
    Copy the code

We answer them all:

Question 1:

This is an interesting thing to say, and it has to do with the order of the calls. If the order of the calls is correct, it is possible to execute the callback before dom updates, for example:

data() {
    return {
      content: 'before'}},mounted() {
    this.test()
  },
  methods: {
  	test() {
        this.nextTick(() = > {
          console.log(this.$refs.box.innerHTML) // called before modifying reactive data
        })
        this.content = 'after'
        this.nextTick(() = > {
          console.log(this.$refs.box.innerHTML)
        })
  	}
  }
// Print the result:
// before
// after

Copy the code

Ok, so now that we know that this stuff is sequentially related, we need some kind of data structure to hold the order of the calls.

Let’s go to see the source: github.com/vuejs/vue/b…

Read the notes in the order β‘ – > β‘₯

let pending = false // Prevent the timerFunc function from being executed repeatedly
const callbacks = [] NextTick (callback); nextTick(callback)
export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  callbacks.push(() = > { // insert the callback into the array
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true // Because timerFunc is an asynchronous call, the timerFunc function may be called repeatedly without control. From this we can also see that while timerFunc is waiting in the asynchronous queue, the callbacks array will accumulate the functions passed in when nextTick is called.
    timerFunc() FlushCallbacks = flushCallbacks = flushCallbacks = flushCallbacks = flushCallbacks = flushCallbacks}...// There is some code that is not relevant to this article, delete it
}

timerFunc = () = > { (4) This function pushes the flushCallbacks into the asynchronous call stack, regardless of whether it uses setTimeout or promise. Then or MutationObserver or setImmediate
    setImmediate(flushCallbacks) // See below πŸ‘‡πŸ»
}

function flushCallbacks () { // call the array callbacks
  pending = false // The callbacks will be copied and emptied, so there is no need to prevent repeated calls to timerFunc
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // β‘₯ Execute the function saved in callbacks!}}Copy the code

Ok, now we know how the call-by-call callback function is implemented.

But here comes a new problem: πŸ‘‡πŸ»πŸ‘‡πŸ» disk

How does modifying responsive data relate to nextTick? I didn’t call it!

This is related to the responsive principle, let’s look at the source code:

Github.com/vuejs/vue/b…

As we all know, the reactive principle of VUE is similar to the published-subscribe model. For each reactive data, there is a corresponding event center, in which a bunch of watcher will register and wait for the notification of data change, so let’s simulate:

The this.content = ‘after’ assignment is intercepted by the setter and notified once, at which point watcher.update() is called.

The update function calls queueWatcher, another function.

QueueWatcher executes this line nextTick(flushSchedulerQueue). The flushSchedulerQueue is responsible for executing all the collected Watcher.

OK, found out where to call nextTick!

The source code is as follows:

Github.com/vuejs/vue/b…

  /** * Subscriber interface. * Will be called when a dependency changes. */ is called when a dependency changes
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)// Call call call call call call call call call}}Copy the code

Github.com/vuejs/vue/b…

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if(! flushing) { queue.push(watcher)// ⚠️⚠️⚠️ pushes all watchers into a queue for continuous execution. ⚠ ️ ⚠ ️ ⚠ ️
    } 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) { waiting =true.// There are some irrelevant code, delete
      nextTick(flushSchedulerQueue) // This is where nextTick is called}}}Copy the code

Question 2: In reactive data modification, all changes are merged and unified. How is this implemented?

There are two steps to modify responsive data:

Step1 collect the watcher with a queue.

Step2 asynchronously call the flushSchedulerQueue function to clear the queue.

Step1 queueWatcher collection operation depends on the function, just in a question at the end of πŸ‘† 🏻 πŸ‘† 🏻 πŸ‘† 🏻 πŸ‘† 🏻 πŸ‘† 🏻 πŸ‘† 🏻 πŸ‘† 🏻 πŸ‘† 🏻 πŸ‘† 🏻, see ⚠ ️ annotations.

FlushSchedulerQueue (flushSchedulerQueue) {flushSchedulerQueue (flushSchedulerQueue);

Github.com/vuejs/vue/b…

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  queue.sort((a, b) = > a.id - b.id)
  // ⚠️ executes all collected watcher⚠️ at once
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index] 
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    ... // There are some irrelevant code, delete}}Copy the code

summary

This is the end of the source code analysis, the responsive principle of the source code is not here to continue in depth, and then write off the topic.

If you are interested, you can search for the following content in vue2.6 source code, with related articles, and explore by yourself:

export function defineReactive // src/core/observer/index.jsFor the component responsive object //src/core/observer/index.js
set: function reactiveSetter (newVal) {    
      const value = getter ? getter.call(obj) : val/ *eslint-disable no-self-compare* /if (newVal === value || (newVal ! == newVal && value ! == value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if(getter && ! setter)return
      if (setter) {
        setter.call(obj, newVal)
      } else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify()// Setter intercepts assignment and fires a notification
    }

// src/core/observer/dep.js
notify () { 
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if(process.env.NODE_ENV ! = ='production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) = > a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // Call the update function to form a closed loop}}Copy the code

Is there a change in the implementation of NextTick in Vue3.0?

There are! Extract a piece of source code. As you can see, use promise.then() to store & concatenate the various callback functions to ensure the order of the calls. Or just return a promise and let the user control with async/await

// https://github.com/vuejs/vue-next/blob/44996d1a0a2de1bc6b3abfac6b2b8b3c969d4e01/packages/runtime-core/src/scheduler.ts#L 42
export function nextTick(
  this: ComponentPublicInstance | void, fn? : () = >void
) :Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

Copy the code

NextTick is based on macro tasks or micro tasks

This is dependent on the runtime environment. In the source code, the implementation is wrapped as the function timerFunc before it is used. So, at the code level, the nextTick function doesn’t care whether the timerFunc implementation is a microtask or a macro task. In Vue 3, the implementation of 2.6 has been abandoned in favor of promise.then(), a component chain structure.

You can read the 2.6.11 source code: github.com/vuejs/vue/b…