欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

简单理解Vue中的nextTick方法

程序员文章站 2022-03-21 19:32:06
vue中的nexttick涉及到vue中dom的异步更新,感觉很有意思,特意了解了一下。其中关于nexttick的源码涉及到不少知识,很多不太理解,暂且根据自己的一些感悟介...

vue中的nexttick涉及到vue中dom的异步更新,感觉很有意思,特意了解了一下。其中关于nexttick的源码涉及到不少知识,很多不太理解,暂且根据自己的一些感悟介绍下nexttick。

一、示例

先来一个示例了解下关于vue中的dom更新以及nexttick的作用。

模板

<div class="app">
 <div ref="msgdiv">{{msg}}</div>
 <div v-if="msg1">message got outside $nexttick: {{msg1}}</div>
 <div v-if="msg2">message got inside $nexttick: {{msg2}}</div>
 <div v-if="msg3">message got outside $nexttick: {{msg3}}</div>
 <button @click="changemsg">
  change the message
 </button>
</div>

vue实例

new vue({
 el: '.app',
 data: {
  msg: 'hello vue.',
  msg1: '',
  msg2: '',
  msg3: ''
 },
 methods: {
  changemsg() {
   this.msg = "hello world."
   this.msg1 = this.$refs.msgdiv.innerhtml
   this.$nexttick(() => {
    this.msg2 = this.$refs.msgdiv.innerhtml
   })
   this.msg3 = this.$refs.msgdiv.innerhtml
  }
 }
})

点击前

简单理解Vue中的nextTick方法

点击后

简单理解Vue中的nextTick方法

从图中可以得知:msg1和msg3显示的内容还是变换之前的,而msg2显示的内容是变换之后的。其根本原因是因为vue中dom更新是异步的(详细解释在后面)。

二、应用场景

下面了解下nexttick的主要应用的场景及原因。

在vue生命周期的created()钩子函数进行的dom操作一定要放在vue.nexttick()的回调函数中

在created()钩子函数执行的时候dom 其实并未进行任何渲染,而此时进行dom操作无异于徒劳,所以此处一定要将dom操作的js代码放进vue.nexttick()的回调函数中。与之对应的就是mounted()钩子函数,因为该钩子函数执行时所有的dom挂载和渲染都已完成,此时在该钩子函数中进行任何dom操作都不会有问题 。

在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的dom结构的时候,这个操作都应该放进vue.nexttick()的回调函数中。

具体原因在vue的官方文档中详细解释:

vue 异步执行 dom 更新。只要观察到数据变化,vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 dom 操作上非常重要。然后,在下一个的事件循环“tick”中,vue 刷新队列并执行实际 (已去重的) 工作。vue 在内部尝试对异步队列使用原生的 promise.then 和messagechannel,如果执行环境不支持,会采用 settimeout(fn, 0)代替。

例如,当你设置vm.somedata = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 dom 状态更新后做点什么,这就可能会有些棘手。虽然 vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 dom,但是有时我们确实要这么做。为了在数据变化之后等待 vue 完成更新 dom ,可以在数据变化之后立即使用vue.nexttick(callback) 。这样回调函数在 dom 更新完成后就会调用。

三、nexttick源码浅析

作用

vue.nexttick用于延迟执行一段代码,它接受2个参数(回调函数和执行回调函数的上下文环境),如果没有提供回调函数,那么将返回promise对象。

源码

/**
 * defer a task to execute it asynchronously.
 */
export const nexttick = (function () {
 const callbacks = []
 let pending = false
 let timerfunc

 function nexttickhandler () {
  pending = false
  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 */
 if (typeof promise !== 'undefined' && isnative(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 (!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
  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 */
  timerfunc = () => {
   settimeout(nexttickhandler, 0)
  }
 }

 return function queuenexttick (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()
  }
  if (!cb && typeof promise !== 'undefined') {
   return new promise((resolve, reject) => {
    _resolve = resolve
   })
  }
 }
})()

首先,先了解nexttick中定义的三个重要变量。

  1. callbacks:用来存储所有需要执行的回调函数
  2. pending:用来标志是否正在执行回调函数
  3. timerfunc:用来触发执行回调函数

接下来,了解nexttickhandler()函数。

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

这个函数用来执行callbacks里存储的所有回调函数。

接下来是将触发方式赋值给timerfunc。

先判断是否原生支持promise,如果支持,则利用promise来触发执行回调函数;

否则,如果支持mutationobserver,则实例化一个观察者对象,观察文本节点发生变化时,触发执行所有回调函数。

如果都不支持,则利用settimeout设置延时为0。

最后是queuenexttick函数。因为nexttick是一个即时函数,所以queuenexttick函数是返回的函数,接受用户传入的参数,用来往callbacks里存入回调函数。

简单理解Vue中的nextTick方法

上图是整个执行流程,关键在于timefunc(),该函数起到延迟执行的作用。

从上面的介绍,可以得知timefunc()一共有三种实现方式。

  1. promise
  2. mutationobserver
  3. settimeout

其中promise和settimeout很好理解,是一个异步任务,会在同步任务以及更新dom的异步任务之后回调具体函数。

下面着重介绍一下mutationobserver。

mutationobserver是html5中的新api,是个用来监视dom变动的接口。他能监听一个dom对象上发生的子节点删除、属性修改、文本内容修改等等。

调用过程很简单,但是有点不太寻常:你需要先给他绑回调:

var mo = new mutationobserver(callback)

通过给mutationobserver的构造函数传入一个回调,能得到一个mutationobserver实例,这个回调就会在mutationobserver实例监听到变动时触发。

这个时候你只是给mutationobserver实例绑定好了回调,他具体监听哪个dom、监听节点删除还是监听属性修改,还没有设置。而调用他的observer方法就可以完成这一步:

var domtarget = 你想要监听的dom节点
mo.observe(domtarget, {
   characterdata: true //说明监听文本内容的修改。
})

简单理解Vue中的nextTick方法

在nexttick中 mutationobserver的作用就如上图所示。在监听到dom更新后,调用回调函数。

其实使用 mutationobserver的原因就是 nexttick想要一个异步api,用来在当前的同步代码执行完毕后,执行我想执行的异步回调,包括promise和 settimeout都是基于这个原因。其中深入还涉及到microtask等内容,暂时不理解,就不深入介绍了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。