Vue’s nextTick understanding

preface

From the beginning, I just wanted to understand a principle of nextTick. Who knows, I could not stop after eating the spicy tiao. From the source code of nextTick to Watcher source code and then to Dep source code, I was shocked, and then combined with the bidirectional binding and responsive system that I had understood before reading the nuggets book, I felt a kind of Epiphany. This is my personal understanding, please big guys advice, if reproduced, please attach the original link, after all, I copy source code is also very tired ~

Say one more word

Because of this article, there are a lot of source code, generally speaking, for me, I will sweep, ten lines, but I! Really! Hope! You can bear it! Go to see! Source code, there will be a drop of notes, be sure to see especially greatly to the author of the notes

If what place write wrong, implore big brothers to advise, mutual progress ~

Please start your performance

So what about nextTick? It’s embarrassing where to start, but let’s look at an example

   <template>
    <div>
      <div ref="username">{{ username }}</div>
      <button @click="handleChangeName">click</button>
    </div>
  </template>
Copy the code
  export default {
    data () {
      return {
        username: 'PDK'}},methods: {
      handleChangeName () {
        this.username = 'Pang Dao Kuan'
        console.log(this.$refs.username.innerText) // PDK}}}Copy the code

Shock!! InnerText = PDK = PDK = PDK = PDK = PDK = PDK = PDK = PDK = PDK = PDK = PDK = PDK

No, let’s look at another example, look at:

  export default {
    data () {
      return {
        username: 'PDK'.age: 18
      }
    },
    mounted() {
      this.age = 19
      this.age = 20
      this.age = 21
    },
    watch: {
      age() {
        console.log(this.age)
      }
    }
  }
Copy the code

This script executes what we guess will print in order: 19,20,21. In practice, however, it prints 21 only once. Why does this happen?

So let’s look at another example

  export default {
    data () {
      return {
        number: 0}},methods: {
      handleClick () {
        for(let i = 0; i < 10000; i++) {
          this.number++
        }
      }
    }
  }
Copy the code

After the handleClick() event is triggered by click, the number is traversed 10,000 times, through the pipeline of setter -> Dep -> Watcher -> Patch -> View in Vue’s bidirectional binding-responsive system. Every time number++ goes through this pipeline to modify the real DOM, the DOM gets updated 10,000 times.

But as a “senior” front-end white, we all know that the front-end is focused on performance, and frequent DOM manipulation, that is a big “taboo” ah. Vue.js is certainly not handled in such an inefficient way. Vue. Js by default, every time a setter is triggered, the Watcher object is pushed into a queue, and the next tick will run the entire queue. The Vue performs DOM updates asynchronously. Whenever a data change is observed, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is triggered more than once, it will only be pushed into the queue once. This removal of duplicate data while buffering is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop, “TICK,” Vue refreshes the queue and performs the actual (de-duplicated) work.

Vue does not modify data immediately, for example, when you set vm.someData = ‘new value’, the component does not immediately re-render. When the queue is refreshed, the component will update the nextTick when the event loop queue empties. In order to wait for the Vue to finish updating the DOM after the data changes, you can use vue.nexttick (callback) immediately after the data changes. This callback will be called after the DOM update is complete. Here is an example from Vue:

  <div id="example">{{message}}</div>
Copy the code
  var vm = new Vue({
    el: '#example'.data: {
      message: '123'
    }
  })
  vm.message = 'new message' // Change the data
  console.log(vm.$el.textContent === 'new message') // false, message has not been updated

  Vue.nextTick(function () {
    console.log(vm.$el.textContent === 'new message') // true, nextTick code executes after DOM update
  })
Copy the code

What the hell is the next tick?

So what on earth is the next tick?

The nextTick function actually does two things. It generates a timerFunc and participates in the event loop as a microTask or macroTask. The second is to put the callback into a Callbacks queue and wait for the appropriate time to execute it

The definition of nextTick on the official website:

A deferred callback is performed after the next DOM update loop ends. Use this method immediately after modifying the data to get the updated DOM.

Before Vue 2.4, microtasks were used, but microtasks were too high priority and could bubble up faster than events in some cases, but using macroTasks could cause performance issues with rendering. So in the new release, microTasks will be used by default, but macroTasks will be used in special cases. Such as v – on. For those who do not know the running mechanism of JavaScript, you can go to ruan Yifeng’s detailed explanation of the running mechanism of JavaScript: talk about Event Loop again, or look at my Event Loop

Back to the point, let’s first look at the source code in Vue:

  /* @flow */
  /* globals MessageChannel */

  import { noop } from 'shared/util'
  import { handleError } from './error'
  import { isIOS, isNative } from './env'

  const callbacks = []  // Define a Callbacks array to simulate the event queue
  let pending = false   // a flag bit that does not need to be pushed again if timerFunc has already been pushed to the task queue

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

  // click the key !!!!! The following English notes are very important !!!!!

  // Here we have async deferring wrappers using both microtasks and (macro) tasks.
  // In < 2.4 we use microtasks everywhere, but there are some scenarios where
  // microtasks have too high a priority and fire in between supposedly
  // sequential events (e.g. #4521, #6690) or even between bubbling of the same
  // event (#6566). However, using (macro) tasks everywhere also has subtle problems
  // when state is changed right before repaint (e.g. #6813, out-in transitions).
  // Here we use microtask by default, but expose a way to force (macro) task when
  // needed (e.g. in event handlers attached by v-on).
  let microTimerFunc
  let macroTimerFunc
  let useMacroTask = false

  // Determine (macro) task defer implementation.
  // Technically setImmediate should be the ideal choice, but it's only available
  // in IE. The only polyfill that consistently queues the callback after all DOM
  // events triggered in the same loop is by using MessageChannel.
  /* istanbul ignore if */
  if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
    macroTimerFunc = (a)= > {
      setImmediate(flushCallbacks)
    }
  } else if (typeofMessageChannel ! = ='undefined' && (
    isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]'
  )) {
    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = flushCallbacks
    macroTimerFunc = (a)= > {
      port.postMessage(1)}}else {
    /* istanbul ignore next */
    macroTimerFunc = (a)= > {
      setTimeout(flushCallbacks, 0)}}// Determine microtask defer implementation.
  /* istanbul ignore next, $flow-disable-line */
  if (typeof Promise! = ='undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    microTimerFunc = (a)= > {
      p.then(flushCallbacks)
      // 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 {
    // fallback to macro
    microTimerFunc = macroTimerFunc
  }

  /** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */
  export function withMacroTask (fn: Function) :Function {
    return fn._withTask || (fn._withTask = function () {
      useMacroTask = true
      const res = fn.apply(null.arguments)
      useMacroTask = false
      return res
   })
 }

  export function nextTick (cb? : Function, ctx? : Object) {
    let _resolve
    callbacks.push((a)= > {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')}}else if (_resolve) {
        _resolve(ctx)
      }
    })
    if(! pending) { pending =true
      if (useMacroTask) {
        macroTimerFunc()
      } else {
        microTimerFunc()
      }
    }
    // $flow-disable-line
    if(! cb &&typeof Promise! = ='undefined') {
      return new Promise(resolve= > {
        _resolve = resolve
      })
    }
  }
Copy the code

Come on, let’s pull it out carefully

The nextTick method is not implemented in the browser platform, so vue. js uses Promise, setTimeout, and setImmediate to create an event in microTask (or Macrotasks). The goal is to execute the event after the current call stack finishes executing (not necessarily immediately)

To implement macrotasks, setImmediate is determined. Failing that, it degrades to MessageChannel. SetTimeout is used. Note that is a judgment on the task of implementing macros

Question? Why define setImmediate and MessageChannel creation in preference to macroTasks rather than setTimeout?

HTML5 stipulates that the minimum time delay of setTimeout is 4ms, which means that the asynchronous callback can be triggered at the earliest 4ms in an ideal environment. Vue uses so many functions to simulate asynchronous tasks for the sole purpose of making callbacks asynchronous and called as soon as possible. MessageChannel and setImmediate have significantly smaller delays than setTimeout

  // Can setImmediate be used
  if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
    macroTimerFunc = (a)= > {
      setImmediate(flushCallbacks)
    }
  } else if (typeofMessageChannel ! = ='undefined' && (
    isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]'
  )) { // Whether you can use MessageChannel
    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = flushCallbacks
    macroTimerFunc = (a)= > {
      port.postMessage(1) // Using the message pipeline, pass 1 to channel.port2 via the postMessage method}}else {
    /* istanbul ignore next */
    macroTimerFunc = (a)= > {
      setTimeout(flushCallbacks, 0)  // Use setTimeout}}Copy the code

Using setTimeout, delay = 0, then flushCallbacks() is used in flushCallbacks

  // setTimeout creates an event called flushCallbacks in MacroTasks. FlushCallbacks will execute all the Cb's in the callbacks in sequence.
  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

As mentioned, nextTick also supports the use of promises and determines whether or not promises are fulfilled

  export function nextTick (cb? : Function, ctx? : Object) {
    let _resolve
    // Consolidate the callback function into an array and push it to the next tick in the queue
    callbacks.push((a)= > {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')}}else if (_resolve) {
        _resolve(ctx)
      }
    })
    if(! pending) {// If pengding = false, no timerFunc has been pushed to the task queue
      pending = true
      if (useMacroTask) {
        macroTimerFunc() // Execute the macro task
      } else {
        microTimerFunc()  // Perform microtasks}}// Determine if you can use promise
    // Assign _resolve if possible
    // The callback is called as a promise
    if(! cb &&typeof Promise! = ='undefined') {
      return new Promise(resolve= > {
        _resolve = resolve
      })
    }
  }
Copy the code

You think this is the end of it?

So I went to Github and looked at the source code of Watcher.js. Back to the beginning of the third example, which is that loop 10000 times, let’s take a look at the source code again, the source code is too much. I picked copy. Well, I’ll make do

  import {
    warn,
    remove,
    isObject,
    parsePath,
    _Set as Set,
    handleError, 
    noop
  } from '.. /util/index'

  import { traverse } from './traverse'
  import { queueWatcher } from './scheduler'                // This is very important, familiar it
  import Dep, { pushTarget, popTarget } from './dep'  // Add watcher to Dep

  import type { SimpleSet } from '.. /util/index'

  let uid = 0   // This is also important, familiar it

  /** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */
  export default class Watcher {
    // SOME of them I don't know, I can only take it literally, if there are any big guys, please let me know
    vm: Component;
    expression: string;
    cb: Function;
    id: number;
    deep: boolean;
    user: boolean;
    lazy: boolean; 
    sync: boolean;
    dirty: boolean;
    active: boolean;
    deps: Array<Dep>;
    newDeps: Array<Dep>;
    depIds: SimpleSet;
    newDepIds: SimpleSet;
    before: ?Function;
    getter: Function; value: any; . constructor ( vm: Component,expOrFn: string | Function.cb: Function,
      options?: ?Object.// Our optionsisRenderWatcher? : boolean ) {this.vm = vm
      if (isRenderWatcher) {
        vm._watch = this
      }
      vm._watchers.push(this)
      // options
      if (options) {
        this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
      } else {
        this.deep = this.user = this.lazy = this.sync = false
      }
      this.cb = cb
      this.id = ++uid       // See, we sort of give each Watcher object a name, and mark each Watcher object with an ID
      this.active = true
      this.dirty = this.lazy 
      this.deps = []
      this.newDeps = []
      this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
        ? expOrFn.toString()
        : ' '
      // parse expression for getter
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn
      } else {
        this.getter = parsePath(expOrFn)
        if (!this.getter) {
          this.getter = noop process.env.NODE_ENV ! = ='production' && warn(
            `Failed watching path: "${expOrFn}"` +
            'Watcher only accepts simple dot-delimited paths. ' +
            'For full control, use a function instead.',
            vm
          )
        }
      }
      this.value = this.lazy
        ? undefined
        : this.get()  // Execute the get() method
    }

    get () {
      pushTarget(this) // Call pushTarget() in Dep
      let value
      const vm = this.vm
      try {
        value = this.getter.call(vm, vm) 
      } catch (e) {
        if (this.user) {
          handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
          throw e
        }
      } finally {
        if (this.deep) {
          traverse(value)
        }
        popTarget() // Call popTarget() in Dep
        this.cleanupDeps()
      }
      return value
    }

    // Add to deP
    addDep(dep: Dep) {
      const id = dep.id // In Dep, there is an array of id and subs (to store all watcher)
      if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
          dep.addSub(this) // Call dep.addSub to add the Watcher object to the array}}}... update () {if (this.lazy) {
        this.dirty = true 
      } else if (this.sync) {
        this.run()
      } else {
        queueWatcher(this) QueueWatcher () method, the source code is given below
      }
    }

    run () {
      if (this.active) {
        const value = this.get()
        if( value ! = =this.value ||
          // Read the English comment!! It's clear!!
          // Deep watchers and watchers on Object/Arrays should fire even
          // when the value is the same, because the value may
          isObject(value) ||
          this.deep
        ) {
          // set new value
          const oldValue = this.value
          this.value = value
          if (this.user) {
            try {
              this.cb.call(this.vm, value, oldValue)
            } catch (e) {
              handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
            this.cb.call(this.vm, value, oldValue) // The callback function}}}}... }Copy the code

Too long? A simple and easy to understand code is provided in The “Anatomy of vue.js” (group owner, I’m not advertising, don’t kick me).

  let uid = 0;

  class Watcher {
    constructor () {
      this.id = ++uid;
    }

    update () {
      console.log('watch' + this.id + ' update');
      queueWatcher(this);
    }

    run () {
      console.log('watch' + this.id + 'View updated ~'); }}Copy the code

What the hell is queueWatcher

Abstract enough! So if you look at this code, if you compare it, you’ll see that there’s a queueWatcher thing, so I went and looked at the source code. Here’s the source code (select Copy)

  import {
    warn,
    nextTick,                       // See, the big Brother we started with appears !!!!
    devtools
  } from '.. /util/index'

  export const MAX_UPDATE_COUNT = 100

  /** * Flush both queues and run the watchers. */
  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.
    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()     // The watcher object calls the run method to execute
      // 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}}}... }/** * !!!!!! * 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  // Get the id of watcher

    // Check whether the id exists, if it already exists, skip directly, if it does not exist, mark hash table has, for the next check
    if (has[id] == null) {
      has[id] = true
      if(! flushing) {// If no flush is flushed, push it to 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 > index && queue[i].id > watcher.id) {
          i--
        }
        queue.splice(i + 1.0, watcher)
      }

      // queue the flush
      if(! waiting) { waiting =true  The flushSchedulerQueue callback is only allowed to be put into callbacks once.

        if(process.env.NODE_ENV ! = ='production' && !config.async) {
          flushSchedulerQueue()
          return
        }
        nextTick(flushSchedulerQueue)  // See, nextTick is called
        The flushSchedulerQueue function in nextTick(flushSchedulerQueue) is an update to the watcher view.
        // Each time it is called, it is pushed into callbacks to be executed asynchronously.}}}Copy the code

Dep

Oops, let’s take a look at the source code in Dep

  import type Watcher from './watcher'          / / familiar it
  import { remove } from '.. /util/index'
  import config from '.. /config'

  let uid = 0

  /** * A dep is an observable that can have multiple * directives subscribing to it. */
  export default class Dep {
    statictarget: ? Watcher; id: number; subs:Array<Watcher>;

    constructor () {
      this.id = uid++
      this.subs = []
    }

    // Add all the Watcher objects to the array
    addSub (sub: Watcher) {
      this.subs.push(sub)
    }

    removeSub (sub: Watcher) {
      remove(this.subs, sub)
    }

    depend () {
      if (Dep.target) {
        Dep.target.addDep(this)
      }
    }

    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)
      }

      // Call each watcher through a loop, and each watcher has an update() method to notify the view of the update
      for (let i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
      }
    }
  }

  // the current target watcher being evaluated.
  // this is globally unique because there could be only one
  // watcher being evaluated at any time.
  Dep.target = null
  const targetStack = []

  export function pushTarget (_target: ? Watcher) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
  }

  export function popTarget () {
    Dep.target = targetStack.pop()
  }

  // To put it more simply, the Watcher instance is stored in the corresponding Dep object
  // Dep.target already points to the new Watcher object
  The get method stores the current Watcher object (dep.target) in its subs array
  // When data changes, set calls notify of the Dep object to notify all its internal Watcher objects of view updates.
Copy the code

Last bit of rambling

Really is to write this article, spent an afternoon, also in Denver to find some articles, but they are not detailed enough, and most of the time, feeling a lot of articles are the same, draw lessons from others’ understanding, and see the interpretation of dye devoted to bosses at the same time, and went to see the source code, just probably read it, and sure enough, the article, more than to see the source code to reality!!!!!!

link

My Blog: github.com/PDKSophia/b…

“Anatomy of vue.js internal operation mechanism” : juejin.cn/book/684473…

Vue official website asynchronous update queue: cn.vuejs.org/v2/guide/re…

MessageChannel API: developer.mozilla.org/zh-CN/docs/…

“DOM Asynchronous Update Strategy and nextTick Mechanism in Vue” : funteas.com/topic/5a8dc…

Vue.js source code nextTick: github.com/vuejs/vue/b…

Vue. Js source code Watcher: github.com/vuejs/vue/b…

Vue. Js source code of Dep: github.com/vuejs/vue/b…