淺析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

歡迎學習交流

相關文章
相關標籤/搜索