nextTick principle

Vue nextTick principle

Usage scenario :
After obtaining data, when you need to perform the next step or other operations on the new view, you find that the DOM cannot be obtained.

This involves a very important concept of Vue: asynchronous update queue (JS running mechanism, event loop) .

Asynchronous update queue

In case you haven't noticed, Vue executes asynchronously when updating the DOM. As long as the data change is detected, the DOM is not directly updated , but a queue is opened , and all data changes that occur in the same event loop are buffered .

If the same watcher is triggered multiple times, it will only be pushed into the queue once. This deduplication during buffering is very important to avoid unnecessary calculations and DOM operations.

Then, on the next event loop "tick", Vue flushes the queue and performs the actual (deduplicated) work.

So if you use a for loop to dynamically change the data 100 times, it will only apply the last change. If there is no such mechanism, the DOM will be redrawn 100 times, which is a lot of overhead and performance loss.

Vue internally tries to use native Promise.then, MutationObserver and setImmediate for asynchronous queues . If the execution environment does not support it, it will use setTimeout(fn, 0) instead.

Such as

// 修改数据
vm.msg = 'Hello'
// 该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,
console.log(vm.$el.textContent) //并不会得到‘hello’

// 这样才可以 nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function () {
 // DOM 更新了
})

// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)  即不传回调
Vue.nextTick()
 .then(function () {
   // DOM 更新了
 })
 // Vue实例方法vm.$nextTick做了进一步封装,把context参数设置成当前Vue实例。

Official definition of Vue.nextTick

Vue.nextTick( [callback, context] )

parameter:

  • {Function} [callback] //Callback function, provides promise call when not passed
  • {Object} [context] // The context in which the callback function is executed. If not passed, it will be automatically bound to the instance that calls it by default.

usage:

Execute the delayed callback after the next DOM update cycle. Use this method immediately after modifying the data to get the updated DOM.

Macro tasks/Micro tasks

First, we need to understand the concepts of EventLoop, macro task, and micro task in the browser.

JS execution is single-threaded and it is based on event loop.
For details, please see Detailed explanation of JavaScript operating mechanism: Let’s talk about Event Loop again

Here is a picture to show the execution relationship between the latter two in the main thread:

execution relationship

When the main thread completes the synchronization task:

  1. The engine first takes out the first task from the macrotask queue, and after execution, takes out all the tasks in the microtask queue, executes them all in order, and UIrenders;
  2. Then take out one from the macrotask queue. After the execution is completed, take out all the ones in the microtask queue again and execute them in sequence, URender;
  3. The cycle repeats until all tasks in both queues are taken.

Common types of asynchronous tasks in browser environments, according to priority:

  • macro task: synchronization code, setImmediate, MessageChannel, setTimeout/setInterval
  • micro task:Promise.then、MutationObserver

After understanding this, we will find:

The order of execution of asynchronous tasks is based on priority. Vue's asynchronous queue uses micro tasks first by default , which uses its high priority characteristics to ensure that all micro tasks in the queue are executed in one cycle.

Let's take a look at the specific implementation of micro task and macro task in the nextTick source code:

First, define the variables:


// 空函数,可用作函数占位符
import { noop } from 'shared/util' 
 // 错误处理函数
import { handleError } from './error'
 // 是否是IE、IOS、内置函数
import { isIE, isIOS, isNative } from './env'
// 使用 MicroTask 的标识符,这里是因为火狐在<=53时 无法触发微任务,在modules/events.js文件中引用进行安全排除
export let isUsingMicroTask = false 

var callbacks = [];   // 存放异步执行的回调
var pending = false;  // 用来标志是否正在执行回调函数
var timerFunc;  // 异步执行函数 用于异步延迟调用 flushCallbacks 函数

Then, create the function that is actually called within $nextTick

// 对callbacks进行遍历,然后执行相应的回调函数
function flushCallbacks  () {
  pending = false;
  //  拷贝出函数数组副本
   // 这里拷贝的原因是:
    // 有的cb 执行过程中又会往callbacks中加入内容
    // 比如 $nextTick的回调函数里还有$nextTick
    // 后者的应该放到下一轮的nextTick 中执行
    // 所以拷贝一份当前的,遍历执行完当前的即可,避免无休止的执行下去
  var copies = callbacks.slice(0);
  //  把函数数组清空
  callbacks.length = 0;
  // 依次执行函数
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

Secondly, Vue will give priority to using native Promise.then and MutationObserver according to the current browser environment. If neither is supported, setTimeout will be used instead. The purpose is to delay the function until the DOM is updated before using it.

Since macro tasks take more time than micro tasks, micro tasks are preferred when browsers support them. If the browser does not support microtasks, then use macrotasks.

  1. Delayed calling of Promise.then
// 在2.5中,我们使用(宏)任务(与微任务结合使用)。
// 然而,当状态在重新绘制之前发生变化时,就会出现一些微妙的问题
// (例如#6813,out-in转换)。
// 同样,在事件处理程序中使用(宏)任务会导致一些奇怪的行为
// 因此,我们现在再次在任何地方使用微任务。
// 优先使用 Promise

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  var logError = function (err) { console.error(err); };
  timerFunc = function () {
    p.then(flushCallbacks ).catch(logError);
     // 用Promise模拟的,但是在iOS UIWebViews中有个bug,Promise.then并不会被触发
    // 除非浏览器中有其他事件触发,例如处理setTimeout。所以手动加了个空的setTimeout
    if (isIOS) { setTimeout(noop); }
  };
}

If the browser supports Promise, then use Promise.then to delay the function call. The Promise.then method can delay the function to the end of the current function call stack, that is, the function is called at the end of the function call stack. thereby achieving delay.

  1. MutationObserver
// 当 原生Promise 不可用时,使用 原生MutationObserver
else if (typeof MutationObserver !== 'undefined' && (
 isNative(MutationObserver) ||
 MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {

 var counter = 1;
  // 创建MO实例,监听到DOM变动后会执行回调flushCallbacks
 var observer = new MutationObserver(flushCallbacks);
 var textNode = document.createTextNode(String(counter));
 observer.observe(textNode, {
   characterData: true // 设置true 表示观察目标的改变 -> 节点内容或节点文本的变动
 });
  // 每次执行timerFunc 都会让文本节点的内容在 0/1之间切换
   // 切换之后将新值复制到 MO 观测的文本节点上
   // 节点内容变化会触发回调->flushCallbacks会被调用
 timerFunc = function () {
   counter = (counter + 1) % 2;
   textNode.data = String(counter);
 };
}

MutationObserver is a new function added by h5. Its function is to monitor the changes of dom nodes, and execute the callback function after all dom changes are completed.
Mutation Observer API


method:

The constructor is
used to instantiate a Mutation observer object. The parameter is a callback function, which is a function that will be executed after the specified DOM node sends changes, and will be passed in two parameters, one is the change record array. (MutationRecord), the other is the observer object itself

let observer = new MutationObserver(function(records, itself){}); //实例化一个Mutation观察者对象

observe
On the observer object, register the DOM nodes that need to be observed, and the corresponding parameters

observer.observe(Node target, optional MutationObserverInit options)

The properties of the optional parameter MutationObserverInit are as follows:

  • childList: Observe the addition and deletion of child nodes of the target node .

  • attributes: observe the attribute changes of the target node

  • characterData: change of node content or node text

  • subtree: Boolean value, changes of all subordinate nodes (including children and children of children)

  • attributeOldValue Boolean value, under the premise that the attributes attribute has been set to true, record the attribute value before the changed attribute node (recorded to the oldValue attribute of the MutationRecord object below)

  • characterDataOldValue On the premise that characterData is a Boolean value and the attribute has been set to true, record the text content before the changed characterData node (record it in the oldValue attribute of the MutationRecord object below)

  • attributeFilter array, indicating the specific attributes that need to be observed (such as ['class', 'src']).


It can be seen that the above code creates a text node to change the content of the text node to trigger the change, because after the data model is updated, the dom node will be re-rendered, so we added such a change monitor, Use a change in a text node to trigger the listener. After all DOM is rendered, execute the function to achieve our delayed effect.

3. setImmediate implementation

This method is only natively implemented in IE and Edge browsers.
Why use setImmediate first instead of directly using setTimeout? It is because HTML5 stipulates that the minimum delay for setTimeout execution is 4ms, and the nested timeout performance is 10ms. In order to be as fast as possible To let the callback execute, setImmediate, which has no minimum delay limit, is obviously better than setTimeout.

else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
 //回退到setImmediate。
//从技术上讲,它利用(宏)任务队列,
//但它仍然是比setTimeout更好的选择。
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
}

4. setTimeOut delayer

else {
    timerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
  }

Using the delay principle of setTimeout, setTimeout(func, 0) will delay the func function to the beginning of the next function call stack, that is, execute the function after the current function is executed, thus completing the delay function.

The priority of delayed calls is as follows:
Promise > MutationObserver > setImmediate > setTimeout

Why micro tasks are used first by default? It is to use its high priority feature to ensure that all micro tasks in the queue are executed in one cycle.

closure function

// src/core/util/next-tick.js
export function nextTick(cb? Function, ctx: Object) {
    let _resolve
    // cb 回调函数会统一处理压入callbacks数组
    callbacks.push(() => {
        if(cb) {
            try {
                cb.call(ctx)
            } catch(e) {
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
    
    // pending 为false 说明本轮事件循环中没有执行过timerFunc()
    if(!pending) {
        pending = true
        timerFunc()
    }
    
    // 当不传入 cb 参数时,提供一个promise化的调用 
    // 如nextTick().then(() => {})
    // 当_resolve执行时,就会跳转到then逻辑中
    if(!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}

next-tick.js exposes the nextTick parameter to the outside world, so it will be executed every time Vue.nextTick is called:

  • First of all, nextTick wraps the incoming cb callback function with try-catch and puts it in an anonymous function and pushes it into the callbacks array. This is done to prevent the execution error of a single cb from causing the entire JS thread to hang up. Wrapping prevents these callback functions from affecting each other if there is an execution error. For example, if the previous one throws an error, the next one can still be executed.
  • Then check the pending status, which is a flag bit, which is false at first and is set to true before entering the timerFunc method, so the next time nextTick is called, it will not enter the timerFunc method. In this method, flushCallbacks will be asynchronous at the next macro/micro tick To execute the tasks collected in the callbacks queue, and the flushCallbacks method will set pending to false at the beginning of execution, so a new round of timerFunc can be started when nextTick is called next time, thus forming the event loop in vue.
  • Finally, check whether cb is passed in, because nextTick also supports Promise-based calls: nextTick().then(() => {}), so if cb is not passed in, a Promise instance is returned directly, and resolve is passed to _resolve, so that when the latter is executed, it will jump to the method passed into then when we call it.

$nextTick

Finally, hang the nexttick function on the Vue prototype and it will be OK.

Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
}

Source code


/* globals MutationObserver */

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

export let isUsingMicroTask = false

const callbacks = [] // 声明公共数组,存储nextTick回调函数
let pending = false

function flushCallbacks () { // 执行timerFunc函数时执行这个回调函数,处理在执行nextTick时新增的方法
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

  // 这里,我们使用微任务异步延迟包装器。
  // 在2.5中,我们使用了(宏)任务(结合了微任务)。
  // 但是,当状态在重新绘制之前更改状态时(例如,#6813,由外而内的过渡),它存在一些细微的问题。
  // 另外,在事件处理程序中使用(宏)任务会导致一些无法规避的怪异行为(例如,#7109,#7153,#7546,#7834,#8109)。
  // 因此,我们现在再次在各处使用微任务。
  // 这种权衡的主要缺点是,在某些情况下,微任务的优先级过高,
  // 并且在假定的顺序事件之间(例如#4521,#6690,它们具有解决方法)甚至在同一事件冒泡之间也会触发(#6566) 。

let timerFunc // 定义全局的timerFunc

// nextTick行为利用了微任务队列,可以通过本机Promise.then或MutationObserver对其进行访问。
//  MutationObserver具有更广泛的支持,但是当在触摸事件处理程序中触发时,
// 它在iOS> = 9.3.3的UIWebView中严重错误。 触发几次后,它将完全停止工作...因此,如果本地Promise可用,
// 我们将使用它:
/* istanbul ignore next, $flow-disable-line */
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]'
)) {
  // 在本地Promise不可用的地方使用MutationObserver,
  // 例如 PhantomJS,iOS7,Android 4.4(#6466 MutationObserver在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)) {
  // 回退到setImmediate。
  // 从技术上讲,它利用了(宏)任务队列,
  // ,但它仍然是比setTimeout更好的选择。
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 后退到setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
  // 重点是这里判断,如果现在是在执行渲染结束的情况,渲染结束了,开始调用
  // 上面被赋值好的 timerFunc ,执行这个函数会
  // 触发执行 flushCallbacks 这个函数,他会遍历执行全部的callbacks
  // 为什么会有那么多的callback呢,因为nextTick每次被执行都会在callbacks中
  // 推送一个事件,形成一个事件组就是 callbacks
  // 这里的pending 是一个全局的变量,默认值false,在flushCallBacks里会把
  // pending = false;此处是一个锁保证nextTick仅有一次执行。
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // 如果没有回调函数,vue会让nextTick返回一个promise对象返回结果
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

Guess you like

Origin blog.csdn.net/kang_k/article/details/111041276