浅析Vue 中 $nextTick 机制

nextTick 出现的前提

由于Vue是异步驱动视图更新数据的,即当咱们在事件中修改数据时,视图并不会即时的更新,而是等在同一事件循环的全部数据变化完成后,再进行视图更新。相似于Event Loop事件循环机制。javascript

官方介绍

首先咱们看下官网给出的介绍:html

Vue.nextTick([callback, context])

  • 参数:vue

    • {Function} [callback]
    • {Object} [context]
  • 用法:java

    在下次DOM更新循环结束以后执行延迟回调。在修改数据以后当即使用这个方法,获取更新后DOM。react

// 修改数据
vm.msg = 'Hello'
// 当咱们在这里调用DOM的数据时,它其实尚未更新
Vue.nextTick(function () {
    // DOM 更新了
})

// 2.1.0新增 Promise用法
Vue.nextTick()
    .then(function () {
    // 此时DOM已经更新
})
复制代码

2.1.0 起新增:若是没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,因此若是你的目标浏览器不原生支持 Promise (IE:大家都看我干吗),你得本身提供 polyfill。ios

DOM更新循环

首先,Vue实现响应式并非在数据改变后就当即更新DOM,而是在一次事件循环的全部数据变化后再异步执行DOM更新.git

有关异步以及事件循环,能够看下我以前写过的一篇关于文章说说异步github

若是不想去详细了解,这边我就简单总结一下事件循环:浏览器

同步代码的执行 => 查找异步队列,进入执行栈,执行Callback1[事件循环1] => 查找异步队列,进入执行栈,执行Callback2[事件循环2] => .....bash

即每一个异步的Callback都会再独立造成一次事件循环

因此咱们能够退出nextTick的触发时机

一次事件循环中的代码执行完毕 => DOM更新 => 触发nextTick的回调 => 进入下一循环

示例展现

Talk is cheap, show me the code. —— Linus Torvalds

可能只凭一些概念性的讲解仍是没法对nextTick机制有很清晰的了解,仍是上个示例来了解一下吧。

<template>
	<div class="app">
        <div ref="contentDiv">{{content}}</div>
        <div>在nextTick执行前获取内容:{{content1}}</div>
        <div>在nextTick执行以后获取内容:{{content2}}</div>
        <div>在nextTick执行前获取内容:{{content3}}</div>
    </div>
</template>

<script>
    export default {
        name:'App',
        data: {
            content: 'Before NextTick',
            content1: '',
            content2: '',
            content3: ''
        },
        methods: {
            changeContent () {
                this.content = 'After NextTick' // 在此处更新content的数据
                this.content1 = this.$refs.contentDiv.innerHTML //获取DOM中的数据
                this.$nextTick(() => {
                    // 在nextTick的回调中获取DOM中的数据
                    this.content2 = this.$refs.contentDiv.innerHTML 
                })
                this.content3 = this.$refs.contentDiv.innerHTML
            }
        },
        mount () {
            this.changeContent()
        }
    }
</script>
复制代码

当咱们打开页面后咱们能够发现结果为:

After NextTick

在nextTick执行前获取内容:Before NextTick

在nextTick执行以后获取内容:After NextTick

在nextTick执行前获取内容:Before NextTick
复制代码

因此咱们能够知道,虽然content1content3得到内容的语句是写在content数据改变语句以后的,但他们属于同一个事件循环中,因此content1content3获取的仍是 'Before NextTick' ,而content2得到内容的语句写在nextTick的回调中,在DOM更新以后再执行,因此获取的是更新后的 'After NextTick'。

应用场景

下面是一些nextTick的主要应用场景

在created 生命周期执行DOM操做

当在created()生命周期中直接执行DOM操做是不可取的,由于此时的DOM并未进行任何的渲染。因此解决办法是将DOM操做写进Vue.nextTick()的回调函数中。或者是将操做放入mounted()钩子函数中

在数据变化后须要进行基于DOM结构的操做

在咱们更新数据后,若是还有操做要根据更新数据后的DOM结构进行,那么咱们应当将这部分操做放入**Vue.nextTick()**回调函数中

这部分的详细缘由在Vue的官方文档中解释的很是清晰:

可能你尚未注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据改变。若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做上很是重要。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工做。Vue 在内部尝试对异步队列使用原生的 Promise.thenMessageChannel,若是执行环境不支持,会采用 setTimeout(fn, 0) 代替。

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

附:nextTick源码解析

我的翻译,如有不妥请随时提出

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]()
    }
  }
  
  // nextTick行为利用了微任务队列
  // 它能够经过原生Promise或者MutationObserver实现
  // MutationObserver已经有了普遍的浏览器支持,然而他仍然在UIWebView在ios系统9.3.3以上的
  // 系统有严重的Bug,问题发生在咱们触摸事件的触发时。
  // 它会在咱们触发一段时间后彻底中止,因此原生Promise是有效能够利用的,咱们会使用它:
  /* 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)
      // 在有问题的 UIWebViews 中,Promise.then 方法不会彻底的中止,但它可能会在一个
      // 奇怪的状态卡住当咱们把回调函数推入一个微任务队列可是这个队列并非在冲洗中,知道
      // 浏览器须要作一些其余的任务时,例如:执行一个定时函数。所以咱们能够"强制"微任务队
      // 列被冲洗经过加入一个空的定时函数
      if (isIOS) setTimeout(noop)
    }
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // 使用MutationObserver当Promise不可用时,
    // 例如 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 {
    // 当MutationObserver 和 Promise都不可使用时
    // 咱们使用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
      })
    }
  }
})()
复制代码

咱们经过源码能够知道,timeFunc这个函数起延迟执行的做用,它有三种实现方式

  • Promise
  • MutationObserver
  • setTimeout

其中PromisesetTimeout 咱们都不陌生,下面重点介绍一下MutationObserver

MutationObserver是HTML5中的新API,是个用来监视DOM变更的接口。他能监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等等。 调用过程很简单,可是有点不太寻常:你须要先给他绑回调:

let mo = new MutationObserver(callback)
复制代码

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

这个时候你只是给MutationObserver实例绑定好了回调,他具体监听哪一个DOM、监听节点删除仍是监听属性修改,尚未设置。而调用他的observer方法就能够完成这一步:

var domTarget = 你想要监听的dom节点
mo.observe(domTarget, {
      characterData: true //说明监听文本内容的修改。
})
复制代码

nextTickMutationObserver的做用就以下图所示。在监听到DOM更新后,调用回调函数。

MutationObserver做用

总结

  • 在同一事件循环中,当全部的同步数据更新执行完毕后,才会调用nextTick
  • 在同步执行环境中的数据彻底更新完毕后,DOM才会开始渲染。
  • 在同一个事件循环中,若存在多个nextTick,将会按最初的执行顺序进行调用。
  • 每一个异步的回调函数执行后都会存在一个独立的事件循环中,对应本身独立的nextTick
  • vue DOM的视图更新实现,,使用到了ES6的Promise及HTML5的MutationObserver,当环境不支持时,使用setTimeout(fn, 0)替代。上述的三种方法,均为异步API。其中MutationObserver相似事件,又有所区别;事件是同步触发,其为异步触发,即DOM发生变化以后,不会马上触发,等当前全部的DOM操做都结束后触发。

参考连接

Vue官方文档-异步更新队列

Ruheng:简单理解Vue中的nextTick

我的Github:Reaper622

欢迎学习交流

相关文章
相关标签/搜索