The source code part of this article uses Vue2. X as the analysis template. It is recommended for students who understand browser event loops, have some basic knowledge of Vue, and have some understanding of bidirectional data binding, publish and subscribe, and observer mode

preface

Scheduling, this word in all walks of life have a lot of related and corresponding design embodiment. For example, the traffic scheduling at the crossroads, the scheduling of food preparation in the kitchen, and the task schedule management of daily work. So the first thing we can do is make an abstract connection from all of these things, and that is, scheduling can be viewed as

Focus on the next execution, asynchronous operations, the behavior that you are not currently eager to practice

Part1: nextTick

The difference is that nextTick is exported from within the framework, whereas $nextTick is mounted on the prototype of Vue, which is called by every instance generated through the Vue constructor.

Let’s look at the official documentation

Defer the callback until after the next DOM update cycle. Use it immediately after you have changed some data to wait for DOM updates.

1-1. Use

In a nutshell, how next-tick is called in code. The following code comes from the Vue official documentation

// You can use it this way
import { nextTick } from 'vue'
const app = createApp({
  setup() {
    const message = ref('Hello! ')
    const changeMessage = async newMessage => {
      message.value = newMessage
      await nextTick()
      console.log('Now DOM is updated')}}})// $nextTick also works
createApp({
  // ...
  methods: {
    // ...
    example() {
      // Modify the data
      this.message = 'changed'
      // DOM has not been updated
      this.$nextTick(function() {
        // DOM is now updated
        // 'this' is bound to the current instance
        this.doSomethingElse()
      })
    }
  }
})
Copy the code

So what’s the difference between these two calls?

Vue attaches nextTick to the frame instance during the rendering phase, so we can call the $nextTick method after the first rendering. There is no direct difference in use between the two, except for some differences in the way they are referenced. Ps: Don’t write anything in the nextTick callback that causes DOM changes… It’s easy to get stuck in a loop

// We can use $nextTick because of this code
export function renderMixin (Vue: Class<Component>) {
/ /...
  Vue.prototype.$nextTick = function (fn: Function) { // Vue is the Vue constructor
    return nextTick(fn, this)}/ /...
}
Copy the code

1-2. NextTick principle

We’ll look at next-tick from the source layer.

Instead of judging the various boundary cases, let’s just simplify the following code by taking the whole next-tick.js.

//next-tick.js 
/ /... Omit the introduction
const callbacks = []
let pending = false

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

let timerFunc

const p = Promise.resolve()
timerFunc = () = > {
  p.then(flushCallbacks)
}
// Omit some special event loops for host environments such as mobile Webview and Native pollyfill

export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  callbacks.push(() = > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}/ /...
  })
  if(! pending) { pending =true
    timerFunc()
  }
  // ...
}
Copy the code

It works by collecting the callbacks in $nextTick in an asynchronous scheduling stack, and executing the stack methods one by one after the current page is rendered. Next-tick. js internally sets the timerFunc callback with a set of boundary conditions. FlushCallbacks will finally execute the callbacks functions in turn when it enters the microtask phase. Whether a task queue performs a macro task or a micro task when it goes off the stack depends on the state itself.

Part2: Scheduling from the framework as a whole

After watching Part1, we know the way and principle of using nextTick. That might raise a question:

  1. Why do you want toDOMCalled after creationnextTick?
  2. The whole Vue schedule depends onnextTickTo achieve?

Before answering my question above, let’s take a look at how Vue renders.

2-1. Update process of Vue

Vue update process official chart

During the mount process, these three steps can be seen as happening:

  1. data changes
  2. Virtual DOM re-render and patch
  3. updated

In between, we also experienced the following process. Let’s combine a simpleMVVMmodel

  1. Triggered by the hijacked objectsetterIn thenotify()
  2. The observer receives notification to execute the observerupdate()The triggerrender
  3. The triggervm.__patch__()【Virtual DOM re-render and patch】
  4. createElm()After the command is executed, mount it.

When building your initial understanding of scheduling, you can think of it as placing the scheduling stack in the Update () phase.

2-2. Scheduling process

Update () calls queueWatcher. QueueWatcher does not update every node as soon as it updates. It queues up the updates that need to be updated, which is a great optimization point in VUE.

Let’s look at it at the source level.

2-2-1. Watcher

In update(), this.sync is the observer method to execute at the moment, and queueWatcher(this) is the stack to trigger.

// watcher.js
  /** * Subscriber interface. * Will be called when a dependency changes. */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */
  run () {
    if (this.active) {
      const value = this.get()
      if( value ! = =this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "The ${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
Copy the code

2-2-2. Scheduling core scheduler.js

After queueWatcher(this) execution in the previous section, let’s take a look at some key source logic in scheduler.js

/** * Empties both queues and executes observer. */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  /* Clear an unprecedented sort in the queue. Make sure: 1. Components are updated from parent to child (because the parent component is always created before the child component) 2. A component user's Watcher runs after the component renders its Watchers. (because the user's Watcher is created before the component renders the Watcher) If the component is destroyed during execution of the parent component's Watcher, the watcher can skip */
  queue.sort((a, b) = > a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run() // Perform the update
    // in dev build, check and stop circular updates.
    if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break}}// keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()// Reset the scheduling status

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')}}function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if(vm._watcher === watcher && vm._isMounted && ! vm._isDestroyed) { callHook(vm,'updated') // Notifies the lifecycle so that methods registered in updated are executed.}}}/** * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if(! flushing) { queue.push(watcher)// Flushing is still the starting value, so let's keep pushing 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) { waiting =true

      if(process.env.NODE_ENV ! = ='production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
Copy the code

After call queueWatcher method, if not the call stack is empty, it would be if it is a development environment, and the vue. Config. The configuration items of js configuration asynchronous, direct call flushSchedulerQueue (), if is normal production environment, see… NextTick (flushSchedulerQueue) that’s going to be connected to nextTick.

Here we can answer the first two questions:

1. WhyDOMCalled after creationnextTick?

A: Because the nextTick callback function is executed in the microtask

2. The whole Vue scheduling depends onnextTickTo achieve?

A: The scheduling of the whole Vue mainly relies on queueWatcher in scheduler.js combined with nextTick. What nextTick represents throughout the design is a tool that can be used as a private function within the framework, but also exposed to framework users through modular exposure and placement in the prototype chain.

conclusion

After analyzing the source code in part2, we can see that Vue has two at the scheduling layer

  • queueWatcher(Running schedule for observer)
  • nextTick(Asynchronous method layer based on event loop)

The whole framework scheduling model is as follows:

Refer to the link

[1] Vue3 official document

[2] Vue2. X official document

[3] Vue source code


Finally, if this article can help you, please support ~ reprint please annotate the source