淺析Vue 中 $nextTick 機制

Reaper622發表於2019-02-26

nextTick 出現的前提

因為Vue是非同步驅動檢視更新資料的,即當我們在事件中修改資料時,檢視並不會即時的更新,而是等在同一事件迴圈的所有資料變化完成後,再進行檢視更新。類似於Event Loop事件迴圈機制。

官方介紹

首先我們看下官網給出的介紹:

Vue.nextTick([callback, context])

  • 引數:

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

    在下次DOM更新迴圈結束之後執行延遲迴調。在修改資料之後立即使用這個方法,獲取更新後DOM。

// 修改資料
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。

DOM更新迴圈

首先,Vue實現響應式並不是在資料改變後就立即更新DOM,而是在一次事件迴圈的所有資料變化後再非同步執行DOM更新.

有關非同步以及事件迴圈,可以看下我之前寫過的一篇關於文章說說非同步

如果不想去詳細瞭解,這邊我就簡單總結一下事件迴圈:

同步程式碼的執行 => 查詢非同步佇列,進入執行棧,執行Callback1[事件迴圈1] => 查詢非同步佇列,進入執行棧,執行Callback2[事件迴圈2] => .....

即每個非同步的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

歡迎學習交流

相關文章