Vue $nextTick

使用場景

1.當改變了DOM中的數據後,立馬去獲取DOM中的數值

2.當我們在watch偵聽器中改變了數據,立馬用這一些數據去做一些操作,這裏爲什麼不可以呢?因爲watch其實應該也算是會被推到隊列中去執行,而nextTick就是等當前DOM更新,也就是隊列全部完成後執行

在vue官網深入響應式原理異步跟新隊列中有寫

3.Vue生命週期的created()鉤子函數進行的DOM操作一定要放在Vue.nextTick()的回調函數中,原因是在created()鉤子函數執行的時候DOM 其實並未進行任何渲染,而此時進行DOM操作無異於徒勞,所以此處一定要將DOM操作的js代碼放進Vue.nextTick()的回調函數中。與之對應的就是mounted鉤子函數,因爲該鉤子函數執行時所有的DOM掛載已完成。

4.當項目中你想在改變DOM元素的數據後基於新的DOM做點什麼,對新DOM一系列的js操作都需要放進Vue.nextTick()的回調函數中;通俗的理解是:更改數據後當你想立即使用js操作新的DOM的時候

5.在使用某個第三方插件時 ,希望在vue生成的某些dom動態發生變化時重新應用該插件,也會用到該方法,這時候就需要在 $nextTick 的回調函數中執行重新應用插件的方法

源碼解析

函數定義

將nextTick定義到Vue原型鏈上代碼位於src/core/instance/render.js,代碼如下

 Vue.prototype.$nextTick = function (fn: Function) {
   return nextTick(fn, this) // 默認this等於Vue.prototype
 }

上述代碼中return的nextTick就是我們本文主角,他的定義如下

export function nextTick (cb?: Function, ctx?: Object) { // next-tick.js line87
  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 => {
      _resolve = resolve
    })
  }
}
  • 函數參數cb?: Function,意味參數cb(callback)的類型爲函數或undefined,ctx(context)同理,這種類型判斷是TypeScript的寫法。
  • 同事可以看出在綁定$nextTick的時候this默認爲Vue.prototype,
  • 函數體內callbacks是在next-tick.js line10定義的一個數組,判斷如果參數cb不爲undefined,就把cb外加(this) push到callbacks中,如果cb爲undefined,則把_resolve(ctx)push到callbacks中。

注意

1.當我們一般在單文件中使用this.$nextTick的時候這個時候this不是Vue.propType,而是單文件中的對象,但是一般那個對象是繼承於Vue的

2.因爲平時使用都是傳回調的,所以很好奇cb什麼情況下會爲undefined,去翻看Vue官方文檔發現:

2.1.0 起新增:如果沒有提供回調且在支持 Promise 的環境中,則返回一個 Promise。

3. 爲什麼使用nextTick的時候可以使用箭頭函數,即nextTick函數定義中的cb.call(ctx)

測試

created() {
  Vue.nextTick(undefined, { a: 'in nextTick' }).then(ctx => {
    console.log(ctx.a) // ctx是對象{ a: 'in nextTick'}
  })
  console.log('out nextTick')
}

輸出:out nextTick  in nextTick

函數體內只剩下中間if (!pending),這段代碼很好懂,pending明顯是一個狀態位,而timerFunc()就應該是nextTick實現異步的核心了

timerFunc

  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      if (isIOS) setTimeout(noop)
    }
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    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 {
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

可以看到nextTick優先使用microTask(Promise和MutationObserver)然後使用macroTask(setImmediate和setTimeout)

flushCallbacks

function flushCallbacks () { // next-tick.js line13
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

因爲callbacks裏都是函數,所以一層淺拷貝的方式就可以滿足複製需求,定義一個copies數組等於callbacks,然後清空callbacks,然後遍歷copies數組調用其中的函數

如何保證只在接收第一個回調函數時執行異步方法?

nextTick源碼中使用了一個異步鎖的概念,即接收第一個回調函數時,先關上鎖,執行異步方法。此時,瀏覽器處於等待執行完同步代碼就執行異步代碼的情況。

打個比喻:相當於一羣旅客準備上車,當第一個旅客上車的時候,車開始發動,準備出發,等到所有旅客都上車後,就可以正式開車了。

當然執行flushCallbacks函數時有個難以理解的點,即:爲什麼需要備份回調函數隊列?執行的也是備份的回調函數隊列?

因爲,會出現這麼一種情況:nextTick套用nextTick。如果flushCallbacks不做特殊處理,直接循環執行回調函數,會導致裏面nextTick中的回調函數會進入回調隊列。這就相當於,下一個班車的旅客上了上一個班車。

這裏還有一個問題是我比較困擾的?第一次使用nextTick的時候就是回立馬執行flushCallbacks這裏其實數組就是一項,但是爲什麼還是要數組呢?其實就是爲了放在第一個nextTick執行的過程中後面添加了新的多個nextTick,所以使用數組

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章