Source Code Analysis of Vue Learning--Vue.js Asynchronous Update DOM Strategy and nextTick(8)

Keywords: Vue Attribute iOS github

Operating DOM

When using vue.js, sometimes you have to operate DOM because of some specific business scenarios, such as:

<template>
  <div>
    <div ref="test">{{test}}</div>
    <button @click="handleClick">tet</button>
  </div>
</template>
export default {
    data () {
        return {
            test: 'begin'
        };
    },
    methods () {
        handleClick () {
            this.test = 'end';
            console.log(this.$refs.test.innerText);//Print "begin"
        }
    }
}

The result of printing is start. Why do we explicitly set test to "end" to get innerText of the real DOM node instead of "end" as we expected, but get the previous value "begin"?

Watcher queue

With doubt, we found the Watch implementation of the Vue.js source code. When a response data changes, its setter function notifies Dep in the closure, and Dep calls all Watch objects it manages. Trigger the update implementation of the Watch object. Let's look at the implementation of update.

update () {
    /* istanbul ignore else */
    if (this.lazy) {
        this.dirty = true
    } else if (this.sync) {
        /*Synchronization executes run direct rendering of views*/
        this.run()
    } else {
        /*Asynchronously pushed to the observer queue, called when the next tick.*/
        queueWatcher(this)
    }
}

We found that Vue.js defaults to use Asynchronous DOM updates.
When update is executed asynchronously, the queueWatcher function is called.

 /*push an observer object into the observer queue. If the same id already exists in the queue, the observer object will be skipped unless it is pushed when the queue is refreshed.*/
export function queueWatcher (watcher: Watcher) {
  /*Get the id of watcher*/
  const id = watcher.id
  /*Check if id exists, skip if it already exists, and mark has h table if it does not exist for next test*/
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      /*If no flush drops, push directly into the queue.*/
      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 >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

Looking at the source code of queueWatcher, we find that the Watch object is not updating the view immediately, but pushed into a queue queue. At this time, the state is waiting. At this time, the Watch object will continue to be pushed into the queue queue. When waiting for the next tick, these Watch objects will be traversed out to update the view. At the same time, the id repetitive Watcher will not be added to the queue many times, because in the final rendering, we only need to care about the final results of the data.

So what is the next tick?

nextTick

vue.js provides a nextTick The function, in fact, is the nextTick called above.

The implementation of nextTick is relatively simple. The purpose of execution is to push a funtion into the microtask or task, and to perform the funtion imported by nextTick after the current stack has been executed (and there will be some tasks that need to be executed in the front row). Take a look at the source code:

/**
 * Defer a task to execute it asynchronously.
 */
 /*
    Delay a task to execute asynchronously, execute at the next tick, execute a function immediately, and return a function
    The purpose of this function is to push a timerFunc into task or microtask and execute it after the current call stack has been executed until the timerFunc is executed.
    The purpose is to defer execution until the current call stack has been executed
*/
export const nextTick = (function () {
  /*Storing callbacks for asynchronous execution*/
  const callbacks = []
  /*A token bit that does not need to be pushed repeatedly if timerFunc is already pushed to the task queue*/
  let pending = false
  /*A function pointer, pointing to the function will be pushed to the task queue, until the main thread task is completed, the timerFunc in the task queue is called*/
  let timerFunc

  /*Callback at the next tick*/
  function nextTickHandler () {
    /*A tag bit that marks the waiting state (that is, the function has been pushed into the task queue or the main thread, and is already waiting for the current stack to execute) so that timerFunc does not need to be pushed into the task queue or the main thread multiple times when push callbacks back to callbacks.*/
    pending = false
    /*Execute all callback s*/
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }

  // the nextTick behavior leverages the microtask queue, which can be accessed
  // via either native Promise.then or MutationObserver.
  // MutationObserver has wider support, however it is seriously bugged in
  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  // completely stops working after triggering a few times... so, if native
  // Promise is available, we will use it:
  /* istanbul ignore if */

  /*
    Explain here that there are three ways to try to get timerFunc: Promise, Mutation Observer, and setTimeout.
    Promise is preferred and Mutation Observer is used in the absence of Promise. Both methods are executed in microtask, earlier than setTimeout, so they are preferred.
    If neither of the above methods supports an environment, setTimeout is used to push the function at the end of the task and wait for the call to execute.
    Reference: https://www.zhihu.com/question/55364497
  */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    /*Using Promise*/
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // in problematic UIWebViews, Promise.then doesn't completely break, but
      // it can get stuck in a weird state where callbacks are pushed into the
      // microtask queue but the queue isn't being flushed, until the browser
      // needs to do some other work, e.g. handle a timer. Therefore we can
      // "force" the microtask queue to be flushed by adding an empty timer.
      if (isIOS) setTimeout(noop)
    }
  } else if (typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // use MutationObserver where native Promise is not available,
    // e.g. PhantomJS IE11, iOS7, Android 4.4
    /*Create a new DOM object of textNode, bind the DOM with Mutation Observer and specify the callback function. When the DOM changes, the callback will be triggered. The callback will be triggered when it enters the main thread (priority over the task queue), that is, textNode.data = String(counter).*/
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    /*Use setTimeout to push callbacks to the end of the task queue*/
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  /*
    Execute when pushed to the next tick in the queue
    cb callback
    ctx context
  */
  return function queueNextTick (cb?: Function, ctx?: Object) {
    let _resolve
    /*cb Save it in callbacks*/
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()

It is an immediate execution function that returns a queueNextTick interface.

The incoming cb is push ed into the callbacks and then timerFunc is executed (pending is a status tag that ensures that timerFunc is executed only once before the next tick).

What is timerFunc?

Look at the source code and find that timerFunc will detect the current environment and different implementations, in fact, according to Promise, Mutation Observer, setTimeout priority, which exist and which use, the worst environment to use setTimeout.

Explain here that there are three ways to try to get timerFunc: Promise, Mutation Observer and setTimeout.
Promise is preferred, Mutation Observer is used in the absence of Promise. Callback functions of both methods are executed in microtask. They are executed earlier than setTimeout, so they are preferred.
If neither of the above methods supports an environment, setTimeout is used to push the function at the end of the task and wait for the call to execute.

Why use microtask first? I learned from Gu Yiling's knowledgeable answers:

When JS event loop is executed, task and microtask are distinguished. The engine will execute microtasks in all microtask queues before each task is executed and the next task is taken from the queue to execute.
SetTimeout callbacks are assigned to a new task to execute, while Promise resolver and Mutation Observer callbacks are scheduled to execute in a new microtask, which is executed prior to the task generated by setTimeout.
To create a new microtask, use Promise first, and try Mutation Observer if the browser does not support it.
Not really. You can only create task with setTimeout.
Why use microtask?
According to HTML Standard, after each task runs, the UI will be re-rendered, then the data update will be completed in microtask, and the latest UI will be available at the end of the current task.
Conversely, if you create a task to update the data, the rendering will take place twice.

Refer to Gu Yilingzhi's answer: https://www.zhihu.com/question/55364497/answer/144215284

First, Promise, (Promise. resolve (). then () can add its callback in microtask.

Mutation Observer creates a new DOM object of textNode, binds the DOM with Mutation Observer and specifies a callback function, which triggers a callback when the DOM changes and is added to the microtask when textNode.data = String(counter).

setTimeout is the last alternative, adding callback functions to task until they are executed.

In summary, the purpose of nextTick is to generate a callback function to add to task or microtask. After the current stack is executed (there may be other functions in the front), the callback function is called, which acts as an asynchronous trigger (that is, trigger when the next tick).

flushSchedulerQueue

/*Github:https://github.com/answershuto*/
/**
 * Flush both queues and run the watchers.
 */
 /*nextTick Callback function, flush drops two queues and runs watchers at the same time on the next tick*/
function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  /*
    Sort queue s so that they can be guaranteed:
    1.The order of component updates is from parent component to child component, because the parent component is always created before the child component.
    2.User watchers of a component run earlier than render watchers, because user watchers are often created earlier than render watchers
    3.If a component is destroyed during the parent component watcher runs, its watcher execution will be skipped.
  */
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  /*You don't need index = queue. length; index > 0; index -- because you don't cache length, because more watcher objects may be push ed into queue during execution of existing watcher objects.*/
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    /*Delete the Hass tag*/
    has[id] = null
    /*Execute watcher*/
    watcher.run()
    // in dev build, check and stop circular updates.
    /*
      In a test environment, detect whether a watch is in a dead loop
      For example, in such a case
      watch: {
        test () {
          this.test++;
        }
      }
      A hundred watch es continuously executed may represent a dead cycle
    */
    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
  /**/
  /*Get a copy of the queue*/
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  /*Reset scheduler status*/
  resetSchedulerState()

  // call component updated and activated hooks
  /*Make subcomponent states adapted toactiveSimultaneous callactivatedhook*/
  callActivatedHooks(activatedQueue)
  /*callupdatedhook*/
  callUpdateHooks(updatedQueue)

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

Flush Scheduler Queue is the callback function for the next tick. The main purpose is to execute the run function of Watcher to update the view.

Why Update Views Asynchronously

Take a look at the following code

<template>
  <div>
    <div>{{test}}</div>
  </div>
</template>
export default {
    data () {
        return {
            test: 0
        };
    },
    created () {
      for(let i = 0; i < 1000; i++) {
        this.test++;
      }
    }
}

Now there is a case where the value of test is executed 1000 times in a ++ loop when created.
Every time ++, setter - > Dep - > Watcher - > Update - > patch is triggered according to the response.
If the view is not updated asynchronously at this time, then every time ++, DOM updates the view directly, which is very performance-consuming.
So Vue.js implements a queue queue, which will uniformly execute the run of Watcher in the queue at the next tick. At the same time, a Watcher with the same id will not be added to the queue again, so it will not run 1000 times. Eventually, updating the view will only directly change the DOM 0 corresponding to test to 1000.
Ensure that the action of updating view operation DOM is invoked at the next tick after the current stack is executed, which greatly optimizes the performance.

Accessing updated data of real DOM nodes

So we need to access the updated data of the real DOM node after modifying the data in the data. Just like this, we will modify the first example of this article.

<template>
  <div>
    <div ref="test">{{test}}</div>
    <button @click="handleClick">tet</button>
  </div>
</template>
export default {
    data () {
        return {
            test: 'begin'
        };
    },
    methods () {
        handleClick () {
            this.test = 'end';
            this.$nextTick(() => {
                console.log(this.$refs.test.innerText);//Print "end"
            });
            console.log(this.$refs.test.innerText);//Print "begin"
        }
    }
}

Using the $nextTick method of the global API of Vue.js, you can retrieve the updated DOM instance in the callback.

Posted by smerny on Sun, 06 Jan 2019 08:03:11 -0800