從源碼裏解析vue中的nextTick的用法

今天做了一個需求,場景是這樣的:

在頁面拉取一個接口,這個接口返回一些數據,這些數據是這個頁面的一個浮層組件要依賴的,然後我在接口一返回數據就展示了這個浮層組件,展示的同時,上報一些數據給後臺(這些數據就是父組件從接口拿的),這個時候,神奇的事情發生了,雖然我拿到數據了,但是浮層展現的時候,這些數據還未更新到組件上去。

父組件:

<template>
  .....
  <pop ref="pop" :name="name"/>
</template>
<script>
export default {
  .....
  created() {
    ....
    // 請求數據,並從接口獲取數據
    Data.get({
      url: xxxx,
      success: (data) => {
        // 問題出現在這裏,我們賦值以後直接調用show方法,去展現,show方法調用的同時上報數據,而上報的數據這個時候還未更新到子組件
        this.name = data.name
        this.$refs.pop.show()
      }
    })
  }
}
</script>

子組件

<template>
  <div v-show="isShow">
    ......
  </div>
</template>
<script>
export default {
  .....
  props: ['name'],
  methods: {
    show() {
      this.isShow = true
      // 上報
      Report('xxx', {name: this.name})
    }
  }
}
</script>

問題分析:

原因vue官網上有解析

可能你還沒有注意到,Vue 異步執行 DOM 更新。只要觀察到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的所有數據改變。如果同一個 watcher 被多次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免不必要的計算和 DOM 操作上非常重要。然後,在下一個的事件循環“tick”中,Vue 刷新隊列並執行實際 (已去重的) 工作。Vue 在內部嘗試對異步隊列使用原生的 Promise.then 和 MessageChannel,如果執行環境不支持,會採用 setTimeout(fn, 0) 代替。

這句話就是說,當我們在父組件設置this.name=name的時候,vue並不會直接更新到子組件中(dom的更新也一樣未立即執行),而是把這些更新操作全部放入到一個隊列當中,同個組件的所有這些賦值操作,都作爲一個watcher的更新操作放入這個隊列當中,然後等到事件循環結束的時候,一次性從這個隊列當中獲取所有的wathcer執行更新操作。在我們這個例子當中,就是我們在調用show的時候,實際上,我們的this.name=name並未真正執行,而是被放入隊列中。vue的這種做法是基於優化而做的,毋庸置疑,不然我們如果有n多個賦值vue就執行n多個dom更新,那效率將會非常的低效和不可取的。



下文中的更新操作指對data的值進行更新的操作,在vue中,都會被放入隊列異步執行。

解決方案:

1、 使用nextTick來延遲執行show方法(籠統得說,執行所有需要在數據真正更新後的操作

通過上面的分析我們知道,我們的所有的對vue實例的更新操作,都會先被放入一個隊列當中,延遲異步執行,這些異步操作,要麼是microtask,要麼是macrotask(是microtask還是macroktask取決於環境,nextTick的源碼中有所體現),根據事件循環機制,先入隊列的先執行,所以如果我們在nextTick當中執行操作就會變成這樣。



2、 使用setTimeout來延遲執行show方法,原理同上

所以我們的解決方法可以是:

this.name = data.name
setTimeout(() => {
 this.$refs.pop.show()
})

或者

this.name = data.name
this.$nextTick(() => {
 this.$refs.pop.show()
})

前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,羣內有大量PDF可供自取,更有乾貨實戰項目視頻進羣免費領取。

nextTick的實現原理

其實nextTick的實現原理是挺簡單的,簡單點說,就是實現異步,通過不同的執行環境,用不同的方式來實現,保證nextTick裏面的回調函數能夠異步執行。爲什麼要這麼做呢?因爲vue對dom的更新也是異步的呀。

下面貼出源碼:

/**
 * 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
   })
  }
 }
})()

前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,羣內有大量PDF可供自取,更有乾貨實戰項目視頻進羣免費領取。

首先我們看到這個是利用了閉包的特性,返回queueNextTick,所以我們實際調用的nextTick其實就是調用queueNextTick,一調用這個方法,就會把nextTick的回調放入隊列callbacks當中,等到合適的時機,會將callbacks中的所有回調取出來執行,以達到延遲執行的目的。爲啥要用閉包呢,我覺得有兩個原因:

1、共享變量,比如callbacks、pending和timerFunc。

2、避免反覆判斷,即是避免反覆判斷timerFunc是利用Promise還是利用MutationObserver或是setTimeout來實現異步,這是函數柯里化的一種運用。

這裏有兩個最主要的方法需要解釋下:

1、 nextTickHandler
這個函數,就是把隊列中的回調,全部取出來執行,類似於microtask的任務隊列。我們通過調用Vue.$nextTick就會把回調全部放入這個隊列當中,等到要執行的時候,調用nextTickHandler全部取出來執行。

2、 timerFunc
這個變量,它的作用就是通過Promise/Mutationobserver/Settimeout把nextTickHandler放入到真正的任務隊列當中,等到事件循環結束,就從任務隊列當中取出nextTickHandler來執行,nextTickHandler一執行,callbacks裏面的所有回調就會被取出來執行來,這樣就達到來延遲執行nextTick傳的回調的效果。

通過這個簡單的源碼分析,我們可以得出兩個結論

1、nextTick會根據不同的執行環境,異步任務可能爲microtask或者macrotask,而不是固定不變的。所以,如果你想讓nextTick裏面的異步任務統統看成是microtask的話,你會遇到坑的。

2、nextTick的並不能保證一定能獲取得到更新後的dom,這取決於你是先進行數據賦值還是先調用nextTick。比如:

new Vue({
   el: '#app',
   data() {
    return {
     id: 2
    }
   },
   created() {
     
   },
   mounted() {
    this.$nextTick(() => {
     console.log(document.getElementById('id').textContent) // 這裏打印出來的是2,因爲先調用了nextTick
    })
    this.id = 3
   }
 })

前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,羣內有大量PDF可供自取,更有乾貨實戰項目視頻進羣免費領取。

結論

如果想要獲取更新後的DOM或者子組件(依賴父組件的傳值),可以在更新操作之後立即使用Vue.nextTick(callback),注意這裏的先後順序,先進行更新操作,再調用nextTick獲取更新後的DOM/子組件,源碼裏面我們知道nextTick是無法保證一定是能夠獲取得到更新後的DOM/子組件的

以上所述是小編給大家介紹的vue中的nextTick的使用,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回覆大家的。在此也非常感謝大家對腳本之家網站的支持!

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