首頁  >  文章  >  web前端  >  Vue nextTick 機制使用詳解

Vue nextTick 機制使用詳解

php中世界最好的语言
php中世界最好的语言原創
2018-05-15 09:19:351757瀏覽

這次帶給大家Vue nextTick 機制使用詳解,Vue nextTick 機制所使用的注意事項有哪些,以下就是實戰案例,一起來看一下。

我們先來看一段Vue的執行程式碼:

export default {
 data () {
  return {
   msg: 0
  }
 },
 mounted () {
  this.msg = 1
  this.msg = 2
  this.msg = 3
 },
 watch: {
  msg () {
   console.log(this.msg)
  }
 }
}

這段腳本執行我們猜測1000m後會依序列印:1、2、3。但在實際效果中,只會輸出一次:3。為什麼會出現這樣的情況?我們來一探究竟。

queueWatcher

我們定義 watch 監聽 msg ,實際上會被Vue這樣呼叫 vm.$watch(keyOrFn, handler, options) 。 $watch 是我們初始化的時候,為 vm 綁定的一個函數,用於建立 Watcher 物件。那我們來看看Watcher 中是如何處理handler 的:

this.deep = this.user = this.lazy = this.sync = false
...
 update () {
  if (this.lazy) {
   this.dirty = true
  } else if (this.sync) {
   this.run()
  } else {
   queueWatcher(this)
  }
 }
...

初始設定this.deep = this.user = this.lazy = this.sync = false ,也就是當觸發update 更新的時候,會去執行queueWatcher 方法:

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
...
export function queueWatcher (watcher: Watcher) {
 const id = watcher.id
 if (has[id] == null) {
  has[id] = true
  if (!flushing) {
   queue.push(watcher)
  } else {
   // if already flushing, splice the watcher based on its id
   // if already past its id, it will be run next immediately.
   let i = queue.length - 1
   while (i > index && queue[i].id > watcher.id) {
    i--
   }
   queue.splice(i + 1, 0, watcher)
  }
  // queue the flush
  if (!waiting) {
   waiting = true
   nextTick(flushSchedulerQueue)
  }
 }
}

這裡面的nextTick(flushSchedulerQueue) 中的flushSchedulerQueue 函數其實就是watcher 的視圖更新:

function flushSchedulerQueue () {
 flushing = true
 let watcher, id
 ...
 for (index = 0; index < queue.length; index++) {
  watcher = queue[index]
  id = watcher.id
  has[id] = null
  watcher.run()
  ...
 }
}

另外,關於waiting 變量,這是很重要的一個標誌位,它保證flushSchedulerQueue 回呼只允許被置入callbacks 一次。接下來我們來看看 nextTick 函數,在說 nexTick 之前,需要你對 Event Loop 、 microTask 、 macroTask 有一定的了解,Vue nextTick 也是主要用到了這些基本原理。如果你還不了解,可以參考我的這篇文章Event Loop 簡介好了,下面我們來看看他的實作:

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]()
  }
 }
 // An asynchronous deferring mechanism.
 // In pre 2.4, we used to use microtasks (Promise/MutationObserver)
 // but microtasks actually has too high a priority and fires in between
 // supposedly sequential events (e.g. #4521, #6690) or even between
 // bubbling of the same event (#6566). Technically setImmediate should be
 // the ideal choice, but it&#39;s not available everywhere; and the only polyfill
 // that consistently queues the callback after all DOM events triggered in the
 // same loop is by using MessageChannel.
 /* istanbul ignore if */
 if (typeof setImmediate !== &#39;undefined&#39; && isNative(setImmediate)) {
  timerFunc = () => {
   setImmediate(nextTickHandler)
  }
 } else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
 )) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = nextTickHandler
  timerFunc = () => {
   port.postMessage(1)
  }
 } else
 /* istanbul ignore next */
 if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // use microtask in non-DOM environments, e.g. Weex
  const p = Promise.resolve()
  timerFunc = () => {
   p.then(nextTickHandler)
  }
 } else {
  // fallback to setTimeout
  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()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
   return new Promise((resolve, reject) => {
    _resolve = resolve
   })
  }
 }
})()

首先Vue透過callback 陣列來模擬事件佇列,事件隊裡的事件,透過nextTickHandler 方法來執行調用,而何事進行執行,是由timerFunc 決定的。讓我們來看看timeFunc 的定義:

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
   setImmediate(nextTickHandler)
  }
 } else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
 )) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = nextTickHandler
  timerFunc = () => {
   port.postMessage(1)
  }
 } else
 /* istanbul ignore next */
 if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // use microtask in non-DOM environments, e.g. Weex
  const p = Promise.resolve()
  timerFunc = () => {
   p.then(nextTickHandler)
  }
 } else {
  // fallback to setTimeout
  timerFunc = () => {
   setTimeout(nextTickHandler, 0)
  }
 }

可以看出timerFunc 的定義優先順序macroTask --> microTask ,在沒有Dom 的環境中,使用microTask ,例如weex

#setImmediate、MessageChannel VS setTimeout

我們是優先定義setImmediate 、 MessageChannel 為什麼優先用他們建立macroTask而不是setTimeout? HTML5規定setTimeout的最小時間延遲是4ms,也就是說理想環境下非同步回呼最快也是4ms才能觸發。 Vue使用這麼多函數來模擬非同步任務,其目的只有一個,就是讓回調異步且儘早呼叫。而MessageChannel 和 setImmediate 的延遲明顯是小於setTimeout的。

解決問題

有了這些基礎,我們再看一次上面提到的問題。因為 Vue 的事件機制是透過事件佇列來調度執行,會等主行程執行空閒後再進行調度,所以先回去等待所有的程序執行完成後再去一次更新。這樣的效能優勢很明顯,例如:

現在有這樣的一種情況,mounted的時候test的值會被 循環執行1000次。每次 時,都會根據響應式觸發 setter->Dep->Watcher->update->run 。如果這時候沒有非同步更新視圖,那麼每次 都會直接操作DOM更新視圖,這是非常消耗效能的。所以Vue實作了一個 queue 佇列,在下一個Tick(或是目前Tick的微任務階段)的時候會統一執行 queue 中 Watcher 的run。同時,擁有相同id的Watcher不會重複加入到該queue中去,所以不會執行1000次Watcher的run。最終更新視圖只會直接將test對應的DOM的0變成1000。保證更新視圖操作DOM的動作是在目前堆疊執行完以後下一個Tick(或是目前Tick的微任務階段)的時候調用,大大優化了效能。

有趣的問題

var vm = new Vue({
  el: '#example',
  data: {
    msg: 'begin',
  },
  mounted () {
   this.msg = 'end'
   console.log('1')
   setTimeout(() => { // macroTask
     console.log('3')
   }, 0)
   Promise.resolve().then(function () { //microTask
    console.log('promise!')
   })
   this.$nextTick(function () {
    console.log('2')
   })
 }
})

這個的執行順序想必大家都知道先後列印:1、promise、2、3。

  1. 因為首先觸發了 this.msg = 'end' ,導致觸發了 watcher 的 update ,從而將更新操作callback push進入vue的事件隊列。

  2. this.$nextTick 也為事件隊列push進入了新的一個callback函數,他們都是透過setImmediate --> MessageChannel --> Promise --> setTimeout 來定義timeFunc 。而 Promise.resolve().then 則是microTask,所以會先去列印promise。

  3. 在支援MessageChannel 和setImmediate 的情況下,他們的執行順序是優先於setTimeout 的(在IE11/Edge中,setImmediate延遲可以在1ms以內,而setTimeout有最低4ms的延遲,所以setImmediate比setTimeout(0)更早執行回呼函數。在不支援MessageChannel 和setImmediate 的情況下,又會透過Promise 定義timeFunc ,也是舊版Vue 2.4 之前的版本會優先執行promise 。這種情況會導致順序成為了:1、2、promise、3。因為this.msg必定先會觸發dom更新函數,dom更新函數會先被callback收納進入非同步時間隊列,其次才定義Promise.resolve().then(function () { console.log('promise!')} ) 這樣的microTask,接著定義$nextTick 又會被callback收納。我們知道隊列滿足先進先出的原則,所以優先去執行callback收納的物件。

  4. 後記

如果你對Vue原始碼感興趣,可以來:更多好玩的Vue約定原始碼解釋相信看了本文案例你已經掌握了方法,更多精彩請關注php中文網其它相關文章!

推薦閱讀:

JS實作透明度漸變功能

#jQuery遍歷XML節點與屬性實作步驟

以上是Vue nextTick 機制使用詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn