Vue2 nextTick source code analysis and detailed implementation principle

References

Vue2 source code

github

Problem Description

The reason for writing this blog is because my friend asked me about the implementation principle of nextTick in vue. I probably know what the situation is, but it is still far from clear to others, so I read the source code of the nextTick part in vue2 .

Que here Xiaowei must study hard hhhhh

nextTick source code

First of all, attach the source code of the nextTick part in vue (v2.7.7) (the comment is removed)

/* globals MutationObserver */

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

export let isUsingMicroTask = false

const callbacks: Array<Function> = []
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


if (typeof Promise !== 'undefined' && isNative(Promise)) {
    
    
  const p = Promise.resolve()
  timerFunc = () => {
    
    
    p.then(flushCallbacks)

    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (
  !isIE &&
  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, 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)) {
    
    

  timerFunc = () => {
    
    
    setImmediate(flushCallbacks)
  }
} else {
    
    
  // Fallback to setTimeout.
  timerFunc = () => {
    
    
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
/**
 * @internal
 */
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
    
    
  let _resolve
  callbacks.push(() => {
    
    
    if (cb) {
    
    
      try {
    
    
        cb.call(ctx)
      } catch (e: any) {
    
    
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
    
    
      _resolve(ctx)
    }
  })
  if (!pending) {
    
    
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    
    
    return new Promise(resolve => {
    
    
      _resolve = resolve
    })
  }
}

Analyze source code

export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
    
    
  let _resolve
  callbacks.push(() => {
    
    
    if (cb) {
    
    
      try {
    
    
        cb.call(ctx)
      } catch (e: any) {
    
    
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
    
    
      _resolve(ctx)
    }
  })
  if (!pending) {
    
    
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    
    
    return new Promise(resolve => {
    
    
      _resolve = resolve
    })
  }
}

The source code of nextTick is actually very simple, and finally exposed a nextTick method to the outside world. In this method, cb is the callback function passed in, and ctx is the object pointed to by this. When there is a callback function cb passed in, push the callback function into the array storing the callback function, and all the callback functions will be pushed into this array, and will be called and executed uniformly.

const callbacks: Array<Function> = []

After pushing into the array, first judge the pending (asynchronous lock) status, the initial status is false, then set pending to true after entering, and call the function timerFunc. Returns a promise-based call when nextTick does not pass in a callback function.

So much has been done in nextTick, let's take a look at what the function timerFunc does

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
    
    
  const p = Promise.resolve()
  timerFunc = () => {
    
    
    p.then(flushCallbacks)

    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (
  !isIE &&
  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, 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)) {
    
    

  timerFunc = () => {
    
    
    setImmediate(flushCallbacks)
  }
} else {
    
    
  // Fallback to setTimeout.
  timerFunc = () => {
    
    
    setTimeout(flushCallbacks, 0)
  }
}

Although this function seems to do a lot of things, it actually judges the current environment. When promises exist in the current environment, promises are used as an asynchronous packaging solution. When there is no promise, use MutationObserver as an asynchronous packaging solution. When there is no MutationObserver, use setImmediate. When setImmediate does not exist, use the safest setTimeout.

Wrap it asynchronously and call the flushCallbacks function

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

flushCallbacks is to reset the asynchronous lock and then call the callback function pushed into the callbacks in a loop.

To sum up, nextTick actually pushes the incoming callback function into an array, and then puts the call of the callback function in this array into an asynchronous task.

After analyzing the source code, let's analyze the principle of nextTick implementation

Preparation

To figure out how nextTick works, first we need to figure out a few things.
1. What is nextTick for
? We know that in Vue, it is not encouraged to operate the dom too much, but what if we must operate the dom? Then there might be a problem. That is, we have operated the dom before the dom has been updated. In this case, we may not get the result we want. At this time, we put nextTick on the outside of the dom, which can often solve the problem.

this.$nextTick(_ => {
    
    
	document.getElementById('box').innerHtml
});

2. What is the js event loop (event loop)
We all know that js is single-threaded. As for why js is single-threaded, it is because the purpose of inventing this language is to serve as a scripting language for browsers. The main purpose of js is to interact with users and operate dom, which determines that js can only be single-threaded, otherwise it will bring very complicated synchronization problems.
Then, since js is single-threaded, the next task will only be executed after the previous task of js is completed. But if one of the tasks takes a long time (for example, there is a 10s timer), then the latter task will have to wait for the previous task to be executed before executing it, which is obviously unscientific.
In this case, js divides tasks into synchronous tasks and asynchronous tasks. When js is executed from top to bottom, when it encounters asynchronous tasks, it will put the asynchronous tasks into the asynchronous execution queue in order, and then wait After the main thread (synchronous task) is all executed, it will go back to execute the tasks in the asynchronous execution queue. In principle, the order of execution is executed in a first-in, first-out manner. The main thread reads events from the task queue. This process is continuously cyclic, so this operating mechanism is called an event loop.


3. After understanding the above event loop for asynchronous macrotasks and asynchronous microtasks, you can see that in the order of execution, I wrote that in principle, they are executed in the order of first-in-first-out, so is the execution order in the asynchronous task queue still Are there exceptions? In fact, there are, because asynchronous tasks are divided into asynchronous microtasks and asynchronous macrotasks. Asynchronous microtasks must be executed before asynchronous macrotasks. Of course, when asynchronous microtasks are executed, they must also enter asynchronous execution according to asynchronous microtasks. The order in the queue is executed. The same is true for asynchronous macro tasks, so what are asynchronous micro tasks and asynchronous macro tasks?

Common ones are:
Microtask: promise.then MutationObserver promise.nextTick
Macrotask: setInterval setTimeout postMessage setImmediate

problem analysis

After making sure that we have understood the above three points, let's formally analyze the principle of nextTick implementation.

To understand the principle of nextTick implementation, it is nothing more than to understand why nextTick will execute it after the dom is updated. Let's think about it, we actually wrote such a code in the code.

this.$nextTick(_ => {
    
    });

But why is this thing so magical, it must be executed after the dom is updated? How does he know that the dom update is complete? Are you dumbfounded and curious? Don't worry, to understand this thing, we have to understand the rendering mechanism of vue.

First of all, we know that when building a vue object, a getter/setter conversion will be performed on the incoming data (the principle of vue responsiveness), we mark it as data, and then when a component is rendered, the corresponding component The watcher (observer) will record the data in the used data as dependent dep, and then when the data in the data changes, it will see which watchers record the changed part of the dependencies, and then notify the watcher to tell If the data under your command has changed, update the view for me quickly. But if a watcher is triggered multiple times, will it trigger to update the view every time it is triggered? The answer is no, because this is very performance-consuming. If a watcher is triggered multiple times, it will only update the result of the last trigger.

And the watcher calls the update method to update the view. We open the Vue source code to find the code of the update part of the watcher.

/**
   * 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()
		/*同步则执行run直接渲染视图*/
      // 基本不会用到sync
    } else {
    
    
      queueWatcher(this)
    }
  }

From the source code, we can see the update method of watcher. Because when the vue data changes, the page will not be updated immediately, but it needs to do deduplication and other things (the multiple trigger watchers mentioned above will only update the last time, so it needs to be deduplicated), so this update uses the queueWatcher method .

Then we find the source code of queueWatcher.

/**
 * 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) {
    
    
    return
  }

  if (watcher === Dep.target && watcher.noRecurse) {
    
    
    return
  }

  has[id] = true
  if (!flushing) {
    
    
    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

    if (__DEV__ && !config.async) {
    
    
      flushSchedulerQueue()
      return
    }
    nextTick(flushSchedulerQueue)
  }
}

Regardless of the previous code, we can see the queueWatcher. Finally, the nextTick method is called to update the view.

At this point, we can probably know the principle of nextTick's operation.

  1. First of all, the data is updated. After listening to the change of the data, the watcher will immediately trigger the update method (this will be executed before the nextTick we wrote in the code), then trigger the queueWatcher, and finally call the nextTick method. After the function call is asynchronously wrapped by nextTick, it is placed in the asynchronous execution queue.

  2. After the previous step is completed, the call of the nextTick callback function we wrote in the code will be asynchronously wrapped and put into the asynchronous execution queue, combined with the execution order of the event loop asynchronous tasks mentioned earlier. Because their asynchronous packaging methods are the same, and the view update method enters the queue first. Therefore, the execution of the nextTick callback written in our code will always be executed after the dom is updated.

In summary, it is the principle of nextTick implementation.

Guess you like

Origin blog.csdn.net/yangxbai/article/details/125931148
Recommended