Vue-nextTick原理

VUE-nextTick原理

一、JS Event Loop

介紹 Vue 的 nextTick 以前,先簡單介紹一下 JS 的運行機制:JS 執行是單線程的,它是基於事件循環的。對於事件循環的理解,阮老師有一篇文章寫的很清楚,大體分爲如下幾個步驟:html

(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。vue

(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。ios

(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。數組

(4)主線程不斷重複上面的第三步。瀏覽器

主線程的執行過程就是一個 tick,而全部的異步結果都是經過 「任務隊列」 來調度被調度。 消息隊列中存放的是一個個的任務(task)。 規範中規定 task 分爲兩大類,分別是 macro task 和 micro task,而且每一個 macro task 結束後,都要清空全部的 micro task網絡

  • 在瀏覽器環境中,常見的 macro tasksetTimeoutMessageChannelpostMessagesetImmediate
  • 常見的 micro taskMutationObseverPromise.then

二、Vue 的 nextTick

Vue 的 nextTick,顧名思義,就是下一個 tick,Vue 內部實現了 nextTick,並把它做爲一個全局 API 暴露出來,它支持傳入一個回調函數,保證回調函數的執行時機是在下一個 tick。官網文檔介紹了 Vue.nextTick 的使用場景:app

Usage: Defer the callback to be executed after the next DOM update cycle. Use it immediately after you’ve changed some data to wait for the DOM update.
使用:在下次 DOM 更新循環結束以後執行延遲迴調,在修改數據以後當即使用這個方法,獲取更新後的 DOM。
在 Vue.js 裏是數據驅動視圖變化,因爲 JS 執行是單線程的,在一個 tick 的過程當中,它可能會屢次修改數據,但 Vue.js 並不會傻到每修改一次數據就去驅動一次視圖變化,它會把這些數據的修改所有 push 到一個隊列裏,而後內部調用 一次 nextTick 去更新視圖,因此數據到 DOM 視圖的變化是須要在下一個 tick 才能完成。

三、源碼

1.  /* @flow */
    
2.  /* globals MessageChannel */
    

4.  import { noop } from 'shared/util'
    
5.  import { handleError } from './error'
    
6.  import { isIOS, isNative } from './env'
    

8.  const callbacks = []
    
9.  let pending = false
    

11.  function flushCallbacks () {
    
12.      pending = false
    
13.      const copies = callbacks.slice(0)
    
14.      callbacks.length = 0
    
15.      for (let i = 0; i < copies.length; i++) {
    
16.          copies[i]()
    
17.      }
    
18.  }
    

20.  // Here we have async deferring wrappers using both micro and macro tasks.
    
21.  // In < 2.4 we used micro tasks everywhere, but there are some scenarios where
    
22.  // micro tasks have too high a priority and fires in between supposedly
    
23.  // sequential events (e.g. #4521, #6690) or even between bubbling of the same
    
24.  // event (#6566). However, using macro tasks everywhere also has subtle problems
    
25.  // when state is changed right before repaint (e.g. #6813, out-in transitions).
    
26.  // Here we use micro task by default, but expose a way to force macro task when
    
27.  // needed (e.g. in event handlers attached by v-on).
    
28.  let microTimerFunc
    
29.  let macroTimerFunc
    
30.  let useMacroTask = false
    

32.  // Determine (macro) Task defer implementation.
    
33.  // Technically setImmediate should be the ideal choice, but it's only available
    
34.  // in IE. The only polyfill that consistently queues the callback after all DOM
    
35.  // events triggered in the same loop is by using MessageChannel.
    
36.  /* istanbul ignore if */
    
37.  if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    
38.      macroTimerFunc = () => {
    
39.          setImmediate(flushCallbacks)
    
40.      }
    
41.  } else if (typeof MessageChannel !== 'undefined' && (
    
42.      isNative(MessageChannel) ||
    
43.      // PhantomJS
    
44.      MessageChannel.toString() === '[object MessageChannelConstructor]'
    
45.  )) {
    
46.      const channel = new MessageChannel()
    
47.      const port = channel.port2
    
48.      channel.port1.onmessage = flushCallbacks
    
49.      macroTimerFunc = () => {
    
50.          port.postMessage(1)
    
51.      }
    
52.  } else {
    
53.      /* istanbul ignore next */
    
54.      macroTimerFunc = () => {
    
55.          setTimeout(flushCallbacks, 0)
    
56.      }
    
57.  }
    

59.  // Determine MicroTask defer implementation.
    
60.  /* istanbul ignore next, $flow-disable-line */
    
61.  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    
62.      const p = Promise.resolve()
    
63.      microTimerFunc = () => {
    
64.          p.then(flushCallbacks)
    
65.          // in problematic UIWebViews, Promise.then doesn't completely break, but
    
66.          // it can get stuck in a weird state where callbacks are pushed into the
    
67.          // microtask queue but the queue isn't being flushed, until the browser
    
68.          // needs to do some other work, e.g. handle a timer. Therefore we can
    
69.          // "force" the microtask queue to be flushed by adding an empty timer.
    
70.          if (isIOS) setTimeout(noop)
    
71.     }
    
72.   } else {
    
73.      // fallback to macro
    
74.      microTimerFunc = macroTimerFunc
    
75.  }
    

77.  /**
    
78.  * Wrap a function so that if any code inside triggers state change,
    
79.  * the changes are queued using a Task instead of a MicroTask.
    
80.  */
    
81.  export function withMacroTask (fn: Function): Function {
    
82.      return fn._withTask || (fn._withTask = function () {
    
83.          useMacroTask = true
    
84.          const res = fn.apply(null, arguments)
    
85.          useMacroTask = false
    
86.          return res
    
87.      })
    
88.  }
    

90.  export function nextTick (cb?: Function, ctx?: Object) {
    
91.       let _resolve
    
92.       callbacks.push(() => {
    
93.         if (cb) {
    
94.             try {
    
95.                 cb.call(ctx)
    
96.             } catch (e) {
    
97.                  handleError(e, ctx, 'nextTick')
    
98.          }
    
99.      } else if (_resolve) {
    
100.            _resolve(ctx)
    
101.     }
    
102.  })
    
103.  if (!pending) {
    
104.      pending = true
    
105.      if (useMacroTask) {
    
106.          macroTimerFunc()
    
107.      } else {
    
108.          microTimerFunc()
    
109.     }
    
110.  }
    
111.  // $flow-disable-line
    
112.  if (!cb && typeof Promise !== 'undefined') {
    
113.       return new Promise(resolve => {
    
114.            _resolve = resolve
    
115.       })
    
116.    }
    
117.  }

這段源碼中 next-tick.js 文件有一段重要的註釋,這裏翻譯一下:異步

在vue2.5以前的版本中,nextTick基本上基於 micro task 來實現的,可是在某些狀況下 micro task 具備過高的優先級,而且可能在連續順序事件之間(例如#4521,#6690)或者甚至在同一事件的事件冒泡過程當中之間觸發(#6566)。可是若是所有都改爲 macro task,對一些有重繪和動畫的場景也會有性能影響,如 issue #6813。vue2.5以後版本提供的解決辦法是默認使用 micro task,但在須要時(例如在v-on附加的事件處理程序中)強制使用 macro task。

這個強制指的是,原來在 Vue.js 在綁定 DOM 事件的時候,默認會給回調的 handler 函數調用 withMacroTask 方法作一層包裝 handler = withMacroTask(handler),它保證整個回調函數執行過程當中,遇到數據狀態的改變,這些改變都會被推到 macro task 中。async

對於 macro task 的執行,Vue.js 優先檢測是否支持原生 setImmediate,這是一個高版本 IE 和 Edge 才支持的特性,不支持的話再去檢測是否支持原生的 MessageChannel,若是也不支持的話就會降級爲 setTimeout 0ide

四、一個小例子
<div id="app">
    <span id='name' ref='name'>{{ name }}</span>
    <button @click='change'>change name</button>
    <div id='content'></div>
</div>
<script>
new Vue({
    el: '#app',
    data() {
        return {
            name: 'SHERlocked93'
    }
},
methods: {
    change() {
        const $name = this.$refs.name
        this.$nextTick(() => console.log('setter前:' + $name.innerHTML))
        this.name = ' name改嘍 '
        console.log('同步方式:' + this.$refs.name.innerHTML)
        setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
        this.$nextTick(() => console.log('setter後:' + $name.innerHTML))
        this.$nextTick().then(() => console.log('Promise方式:' + $name.innerHTML))
     }
  }
})
</script>

執行結果爲:

同步方式:SHERlocked93
setter前:SHERlocked93
setter後:name改嘍
Promise方式:name改嘍
setTimeout方式:name改嘍

解析

  1. 同步方式: 當把data中的name修改以後,此時會觸發namesetter 中的 dep.notify 通知依賴本datarender watcherupdateupdate 會把 flushSchedulerQueue 函數傳遞給 nextTickrender watcherflushSchedulerQueue 函數運行時 watcher.run 再走 diff -> patch 那一套重渲染 re-render 視圖,這個過程當中會從新依賴收集,這個過程是異步的;因此當咱們直接修改了name以後打印,這時異步的改動尚未被 patch 到視圖上,因此獲取視圖上的DOM元素仍是原來的內容。
  2. setter前setter前爲何還打印原來的是原來內容呢,是由於 nextTick 在被調用的時候把回調挨個pushcallbacks數組,以後執行的時候也是 for 循環出來挨個執行,因此是相似於隊列這樣一個概念,先入先出;在修改name以後,觸發把render watcher填入 schedulerQueue 隊列並把他的執行函數 flushSchedulerQueue 傳遞給 nextTick ,此時callbacks隊列中已經有了 setter前函數 了,由於這個 cb 是在 setter前函數 以後被pushcallbacks隊列的,那麼先入先出的執行callbacks中回調的時候先執行 setter前函數,這時並未執行render watcherwatcher.run,因此打印DOM元素仍然是原來的內容。
  3. setter後setter後這時已經執行完 flushSchedulerQueue,這時render watcher已經把改動 patch 到視圖上,因此此時獲取DOM是改過以後的內容。
  4. Promise方式: 至關於 Promise.then 的方式執行這個函數,此時DOM已經更改。
  5. setTimeout方式: 最後執行macro task的任務,此時DOM已經更改。

注意,在執行 setter前 函數 這個異步任務以前,同步的代碼已經執行完畢,異步的任務都還未執行,全部的 $nextTick 函數也執行完畢,全部回調都被push進了callbacks隊列中等待執行,因此在setter前 函數執行的時候,此時callbacks隊列是這樣的:[setter前函數,flushSchedulerQueuesetter後函數,Promise方式函數],它是一個micro task隊列,執行完畢以後執行macro tasksetTimeout,因此打印出上面的結果。
另外,若是瀏覽器的宏任務隊列裏面有setImmediateMessageChannelsetTimeout/setInterval 各類類型的任務,那麼會按照上面的順序挨個按照添加進event loop中的順序執行,因此若是瀏覽器支持MessageChannelnextTick 執行的是 macroTimerFunc,那麼若是 macrotask queue 中同時有 nextTick 添加的任務和用戶本身添加的 setTimeout 類型的任務,會優先執行 nextTick 中的任務,由於MessageChannel 的優先級比 setTimeout的高,setImmediate 同理。

說明

以上部份內容來源與本身複習時的網絡查找,也主要用於我的學習,至關於記事本的存在,暫不列舉連接文章。若是有做者看到,能夠聯繫我將原文連接貼出。

相關文章
相關標籤/搜索