Interpretation of Vue source code -- asynchronous update

Posted by Grim... on Thu, 24 Feb 2022 02:08:32 +0100

When learning becomes a habit, knowledge becomes common sense. Thank you for your likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

preface

Previous Interpretation of Vue source code (3) -- responsive principle Speaking of passing object Defineproperty sets getter s and setter s for each key of the object to intercept data access and settings.

When updating data, such as obj Key = 'new Val' will trigger the interception of setter to detect whether the new value and the old value are equal. If they are equal, do nothing. If they are not equal, update the value, and then dep will notify the watcher to update. Therefore, the entrance point of asynchronous update is the last dep.notify() method in setter.

objective

  • Deep understanding of Vue's asynchronous update mechanism
  • Principle of nextTick

Source code interpretation

dep.notify

/src/core/observer/dep.js

For a more detailed introduction to dep, please check the previous article—— Interpretation of Vue source code (3) -- responsive principle , it doesn't take up space here.

/**
 * Notify all watchers in dep and execute watcher Update() method
 */
notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  // Traverse the watcher stored in dep and execute the watcher update()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

watcher.update

/src/core/observer/watcher.js

/**
 * According to the watcher configuration item, decide how to go next, usually queueWatcher
 */
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    // Go here when lazy execution, such as computed
    // Setting dirty to true allows the computedGetter to recalculate the execution result of the computed callback function when executing
    this.dirty = true
  } else if (this.sync) {
    // Synchronous execution, using VM$ You can pass a sync option when using the watch or watch option,
    // When true, the watcher will not go to the asynchronous update queue and directly execute this run 
    // Method
    // This attribute does not appear in the official document
    this.run()
  } else {
    // When updating, it is usually here to put the watcher into the watcher queue
    queueWatcher(this)
  }
}

queueWatcher

/src/core/observer/scheduler.js

/**
 * Put the watcher into the watcher queue 
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // If the watcher already exists, it will be skipped and will not be re queued
  if (has[id] == null) {
    // Cache watcher ID, used to judge whether the watcher has joined the team
    has[id] = true
    if (!flushing) {
      // Currently, it is not in the status of refreshing the queue. The watcher directly joins the queue
      queue.push(watcher)
    } else {
      // The queue is already being refreshed
      // Traverse in reverse order from the end of the queue according to the current watcher Id find the watcher whose ID is greater than ID, and then insert yourself to the next position after that position
      // Put the current watcher into the sorted queue, and the queue is still in order
      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) {
        // Directly refresh the scheduling queue
        // Generally, Vue is executed asynchronously by default. If it is changed to synchronous execution, the performance will be greatly reduced
        flushSchedulerQueue()
        return
      }
      /**
       * Familiar nexttick = > VM$ nextTick,Vue.nextTick
       *   1,Put the callback function (flushSchedulerQueue) into the callbacks array
       *   2,Add the flushCallbacks function to the browser task queue through the pending control
       */
      nextTick(flushSchedulerQueue)
    }
  }
}

nextTick

/src/core/util/next-tick.js

const callbacks = []
let pending = false

/**
 * Accomplish two things:
 *   1,Wrap the flushSchedulerQueue function with try catch and put it into the callbacks array
 *   2,If pending is false, it means that there is no flushCallbacks function in the browser's task queue
 *     If pending is true, it indicates that the flushCallbacks function has been put into the browser's task queue,
 *     When the flushCallbacks function is to be executed, pending will be set to false again, indicating that the next flushCallbacks function can enter
 *     The browser's task queue
 * pending Function: ensure that there is only one flushCallbacks function in the browser's task queue at the same time
 * @param {*} cb Receive a callback function = > flushschedulerqueue
 * @param {*} ctx context
 * @returns 
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // Use the callbacks array to store the wrapped cb function
  callbacks.push(() => {
    if (cb) {
      // Wrap the callback function with try catch to facilitate error capture
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // Execute timerFunc and put the flushCallbacks function in the browser's task queue (micro task queue is preferred)
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

timerFunc

/src/core/util/next-tick.js

// You can see that the function of timerFunc is very simple, that is, put the flushCallbacks function into the asynchronous task queue of the browser
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  // Promise is preferred resolve(). then()
  timerFunc = () => {
    // Put the flushCallbacks function in the micro task queue
    p.then(flushCallbacks)
    /**
     * In the problematic UIWebViews, promise Then will not be completely interrupted, but it may fall into a strange state,
     * In this state, the callback is pushed into the micro task queue, but the queue is not refreshed until the browser needs to perform other work, such as processing a timer.
     * Therefore, we can "force" refresh the micro task queue by adding an empty timer.
     */
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // MutationObserver followed
  // Use MutationObserver where native Promise is not available,
  // e.g. 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 (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Then there is setImmediate, which is actually a macro task, but it is still better than setTimeout
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Finally, if there is no way, use setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

flushCallbacks

/src/core/util/next-tick.js

const callbacks = []
let pending = false

/**
 * Three things were done:
 *   1,Set pending to false
 *   2,Empty callbacks array
 *   3,Execute each function in the callbacks array (such as flushSchedulerQueue and the callback function passed by the user calling nextTick)
 */
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // Traverse the callbacks array and execute each of the flushSchedulerQueue functions stored in it
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

flushSchedulerQueue

/src/core/observer/scheduler.js

/**
 * Flush both queues and run the watchers.
 * The flush callbacks function is responsible for refreshing the queue. It mainly does the following two things:
 *   1,Updating flushing to true indicates that the queue is being refreshed. During this period, special processing is required when push ing new watcher s into the queue (put them in the appropriate position of the queue)
 *   2,According to the watcher in the queue The ID is sorted from small to large to ensure that the watcher created first is executed first, and also cooperate with the first step
 *   3,Traverse the watcher queue and execute the watcher in turn before,watcher.run and clear the cached watcher
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  // Flag is now refreshing the queue
  flushing = true
  let watcher, id

  /**
   * Sorting the queue before refreshing it (ascending order) can ensure that:
   *   1,The update order of components is from parent to child, because parent components are always created before child components
   *   2,The user watcher of a component is executed before its rendering watcher, because the user watcher is created before the rendering watcher
   *   3,If a component is destroyed during the watcher execution of its parent component, its watcher can be skipped
   * After sorting, new watcher s will also be put into the appropriate position of the queue in order during the refresh of the queue
   */
  queue.sort((a, b) => a.id - b.id)

  // Queue is used directly here Length, which dynamically calculates the length of the queue. There is no cache length because the queue may be push ed into a new watcher during the execution of an existing watcher
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // Execute before hook when using VM$ Watch or watch options can be passed through the configuration item (options.before)
    if (watcher.before) {
      watcher.before()
    }
    // Clear the cached watcher
    id = watcher.id
    has[id] = null

    // Execute the watcher Run, and finally trigger the update function, such as updateComponent or get this xx (xx is the second parameter of user watch). Of course, the second parameter may also be a function, so execute it directly
    watcher.run()
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  /**
   * Reset scheduling status:
   *   1,Reset has cache object, has = {}
   *   2,waiting = flushing = false,Indicates the end of the refresh queue
   *     waiting = flushing = false,It means that the new flushscheduler queue function can be put into the callbacks array, and the next flushCallbacks function can be put into the browser's task queue
   */
  resetSchedulerState()

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

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

/**
 * Reset the scheduler's state.
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

watcher.run

/src/core/observer/watcher.js

/**
 * It is called by the flush scheduler queue function. If it is a synchronous watch, it is called by this Update is called directly to complete the following things:
 *   1,Execute the second parameter passed by instantiating the watcher, updateComponent, or get this A function of XX (the function returned by parsePath)
 *   2,Update old value to new value
 *   3,Execute the third parameter passed when instantiating the watcher, such as the callback function of the user watcher
 */
run () {
  if (this.active) {
    // Call this Get method
    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
    ) {
      // Update old value to new value
      const oldValue = this.value
      this.value = value

      if (this.user) {
        // If it is the user watcher, the callback function, the third parameter passed by the user, will be executed. The parameters are val and oldVal
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        // Render watcher, this CB = NOOP, an empty function
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

watcher.get

/src/core/observer/watcher.js

  /**
   * Execute this Getter and collect dependencies again
   * this.getter It is the second parameter passed when instantiating the watcher, a function or string, such as the function returned by updateComponent or parsePath
   * Why collect dependencies again?
   *   Because triggering the update indicates that the responsive data has been updated, but the updated data has been observe d, but there is no dependency collection,
   *   Therefore, when updating the page, the render function will be executed again, and the read operation will be triggered during execution. At this time, dependency collection will be carried out
   */
  get () {
    // Open target.dep, this = target.dep
    pushTarget(this)
    // value is the result of the callback function execution
    let value
    const vm = this.vm
    try {
      // Execute the callback function, such as updateComponent, and enter the patch phase
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // Close Dep.target, Dep.target = null
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

The above is the whole execution process of Vue asynchronous update mechanism.

summary

  • The interviewer asked: how is Vue's asynchronous update mechanism implemented?

    Answer:

    The core of Vue's asynchronous update mechanism is realized by using the browser's asynchronous task queue. Micro task queue is preferred, followed by macro task queue.

    When the responsive data is updated, the dep.notify method will be called to notify the watcher collected in dep to execute the update method Update puts the watcher itself into a watcher queue (global queue array).

    Then, a method to refresh the watcher queue (flushscheduler queue) is put into a global callback array through the nextTick method.

    If there is no function called flushCallbacks in the asynchronous task queue of the browser at this time, execute the timerFunc function and put the flushCallbacks function into the asynchronous task queue. If the flushCallbacks function already exists in the asynchronous task queue, wait for its execution to complete before putting it into the next flushCallbacks function.

    The flushCallbacks function is responsible for executing all flushSchedulerQueue functions in the callbacks array.

    The flushscheduler queue function is responsible for refreshing the watcher queue, that is, executing the run method of each Watcher in the queue array to enter the update phase, such as executing the component update function or the callback function of the user watch.

    The complete execution process is actually the process of reading the source code today.

Interview question: how is Vue's nextTick API implemented?

Answer:

Vue.nextTick or VM$ The principle of nexttick is actually very simple. It does two things:

  • Wrap the passed callback function with try catch and put it into the callbacks array
  • Execute the timerFunc function and put a function to refresh the callbacks array in the asynchronous task queue of the browser

link

Thank you for your likes, collections and comments. I'll see you next time.

When learning becomes a habit, knowledge becomes common sense. Thank you for your likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

Topics: Javascript Front-end ECMAScript TypeScript Vue.js