首頁 > web前端 > js教程 > Vue nextTick 機制使用詳解

Vue nextTick 機制使用詳解

php中世界最好的语言
發布: 2018-05-15 09:19:35
原創
1972 人瀏覽過

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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 的:

1

2

3

4

5

6

7

8

9

10

11

12

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 方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

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 的視圖更新:

1

2

3

4

5

6

7

8

9

10

11

12

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 簡介好了,下面我們來看看他的實作:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

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 的定義:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

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的微任務階段)的時候調用,大大優化了效能。

有趣的問題

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板