preface

Because the current project is using vue. js, recently I have also been looking at vue. js source code, based on which I made the following summary and output. This chapter is mainly about Vue asynchronous update mechanism and source code implementation analysis.

dep.notify

According to Vue’s reactive principle, when a setter method for a data is fired, its setter function notifys the Dep in the closure, and the Dep calls all watcher objects it manages. Trigger the Update implementation of the Watcher object.

dep.notify

/src/core/observer/dep.js

  // Notify all watchers in the DEP to execute the update method in watcher
  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)
    }
    // Go through the watcher stored in the DEP and execute watcher.update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
Copy the code

watcher.update

/src/core/observer/watcher.js

 /** * Subscriber interface. * Will be called when a dependency changes. * Decide which process to go according to watcher configuration item, usually queueWatcher */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // Use this logic when executing lazily, such as compted
      // Set dirty to true, and perform computed getter when reactive data is updated again after component updates
      // Re-execute the computed callback function, calculate the new value, and then cache it to watcher.value
      this.dirty = true
    } else if (this.sync) {
      // Execute the run function directly to render the view during synchronization
      this.run()
    } else {
      // Put watcher into the Watcher queue
      queueWatcher(this)}}Copy the code

queueWatcher

/src/core/observer/scheduler.js

/** * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the Queue is being flushed. * Uploading watcher to watcher queue */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // If watcher already exists, the queue will be skipped
  if (has[id] == null) {
    // Caches watcher. Id to determine whether watcher is in the queue
    has[id] = true
    if(! flushing) {// If the queue status is not refreshed, watcher is placed directly in the queue
      queue.push(watcher)
    } else {
      // If you are already flushing the queue state, find the position greater than its watcher.id based on the current watcher.id and insert yourself into the next position after that position
      // Put the current watcher into the queue, keeping the queue in order
      // 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) {
        // Execute synchronously in a non-production environment and set asynchrony to false
        flushSchedulerQueue()
        return
      }
      /** * this.$ue. NextTick. Put the flushSchedulerQueue into the callbacks array * 2. Add the flushCallbacks function */ to the browser task queue using pending control
      nextTick(flushSchedulerQueue)
    }
  }
}

Copy the code

QueueWatcher code tells us that instead of updating the view immediately, the Watcher object is pushed into a queue in a waiting state, and then watcher objects will continue to be pushed into the queue, When the next tick runs and the queue is pulled out and run, the Watcher objects will be iterated and updated. Also, a watcher with a duplicate ID is not added to the queue more than once. This also explains why the same Watcher is triggered more than once and only gets pushed into the queue once. Based on this, we can understand the process as follows:

Vue updates the DOM asynchronously to avoid frequent DOM manipulation. These asynchronous operations are queued as cb tasks (microtasks first) through the nextTick function, and are executed each time the tick ends, updating the DOM.

nextTick

const callbacks = []
let pending = false

 /** * 1. Wrap the CB function with a try catch and place it in the callbacks array * 2. Determine pending value, If pENING is true, the pending function of the browser task queue has been set to flushCallbacks. Ensure that there is only one flushCallbacks function * in the browser's task queue at any one time@param {*} Cb receives a callback function *@param {*} Context in which the CTX callback function is executed *@returns * /
export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  // Place the incoming callback into the Callbacks array
  callbacks.push(() = > {
    if (cb) {
      // Wrap callback functions with try catch to facilitate error catching
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    // Execute timerFunc and place the flushCallbacks function in the browser's task queue (microtask queue preferred)
    // Mediate is used to determine the priority of Promise, MutationObserver, setImmediate, and setTimeout
    timerFunc()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

timerFunc

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

// timerFunc simply puts the flushCallbacks function into the browser's asynchronous task queue
let timerFunc
// Judgment 1: Whether to support Promises natively
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  / / preferred Promise. Resolve (). Then ()
  timerFunc = () = > {
    Put the flushCallbacks function in the microtask queue
    p.then(flushCallbacks)
    /** * In UIWebViews in question, promise.then does not break completely, but it may fall into a weird state, * in which the callback is pushed to the microtask queue, but the queue is not refreshed until the browser needs to do something else, such as handle a timer. * Therefore, we can "force" the microtask queue to refresh by adding an empty timer. * /
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
  // Judgment 2: Whether MutationObserver is supported natively
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  / / MutationObserver times
  // Use MutationObserver where native Promise is not available,
  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
  // Judgment 3: Whether setImmediate is native supported
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  // setImmediate, which is already a macro task, is still better than setTimeout
  timerFunc = () = > {
    setImmediate(flushCallbacks)
  }
  // Judgment 4: None of the above, use setTimeout directly
} else {
  // Use setTimeout
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}Copy the code

flushCallbacks

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

const callbacks = []
// Pending Indicates that the pending can be executed only once at a time
let pending = false
/** * Set pending to false * empty the Callbacks array * execute every function in the callbacks array */
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
Copy the code

flushSchedulerQueue

/src/core/observer/scheduler.js

Flush both queues and run the watchers. FlushCallbacks in flushCallbacks do two things: * if the number of watchers. id in the queue is larger than the number of watchers. id in the queue, the number of watchers. id in the queue is larger than the number of watchers. id in the queue. * * * * * * * * * * * * * * * * * * * * * * * *
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  // the flag is now flushing the queue
  flushing = true
  let watcher, id

  /** * The queue is sorted (ascending) before refreshing the queue to ensure that: * 2. A component's user watcher is executed before its rendering watcher. 3. If a component is destroyed during the execution of its parent component's Watcher, Then its watcher can be skipped * after sorting, new watcher entries will be placed in the appropriate place in the queue */ during queue refresh
  queue.sort((a, b) = > a.id - b.id)

  Queue.length is used directly to dynamically calculate the length of the queue. There is no cache length because new watcher may be pushed into the queue during execution of the existing watcher
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // Execute the before hook, which can be passed by the configuration item (options.before) when using vm.$watch or the watch option
    if (watcher.before) {
      watcher.before()
    }
    // Clear the cached watcher
    id = watcher.id
    has[id] = null

    // Execute watcher. Run to trigger an update function, such as updateComponent, or get this.xx (xx is the second argument to the user's watch)
    watcher.run()
  }

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

  /** * Reset scheduling status: * waiting = flushing = false * waiting = flushing = false * waiting = flushing = false The new flushSchedulerQueue function can be added to the callbacks array, and the next flushCallbacks function can be added to 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
}


Copy the code
/** * this is called from the flushSchedulerQueue function, or from this.update if the watch is synchronized, to do the following: * 1, execute the second parameter passed by watcher UpdateComponent or get a function of this.xx (the function returned by parsePath) * 2, update the old value to the new value * 3, and perform the third argument passed when instantiating Watcher, such as the user watcher callback function */
run () {
  if (this.active) {
    // Call the 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 the old value to the new value
      const oldValue = this.value
      this.value = value

      if (this.user) {
        // If it is the user watcher, the third argument passed by the user -- the callback function val and oldVal -- is executed
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
        // Render watcher, this.cb = noop, an empty function
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}
Copy the code

conclusion

The core of Vue’s asynchronous update mechanism is to use the browser’s asynchronous task queue to achieve, the preferred micro task queue, macro task queue.

  1. The traversal property adds get, set methods that collect dependencies (dev.subs.push(watcher)), and set methods that call notify of deP, The purpose of this method is to notify all the Watcher collected in the DEP and call the Update method of the Watcher, which we can think of as publish and subscribe in design mode.

  2. By default, the update method is invoked to trigger the queueWatcher function, whose main function is to queue the watcher instance itself (queue.push(watcher)), NextTick (flushSchedulerQueue) is then called.

  3. The nextTick method is then used to flush the Watcher queue into a global callbacks queue, and the callbacks are then iterated through and executed asynchronously (in this case, the asynchronous update queue). If the browser does not have a function named flushCallbacks in the asynchronous task queue, the timerFunc function is executed to place the flushCallbacks function in the asynchronous task queue. If the asynchronous task queue already has the flushCallbacks function, wait until its execution is complete before adding the next flushCallbacks function.

  4. FlushSchedulerQueue is a function that calls the watcher.run method of all watchers in the queue to update it. After the run method is called, the next operation is to generate a new real DOM by diff arithmetic with the old virtual DOM

  5. The flushSchedulerQueue is called watcher.run() when it is executed, and you see a new page

reference

Vue source code analysis easy to understand Vue asynchronous update mechanism