介紹 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 task
有 setTimeout
、MessageChannel
、postMessage
、setImmediate
;micro task
有 MutationObsever
和 Promise.then
。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 0
。ide
<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改嘍
解析
data
中的name
修改以後,此時會觸發name
的 setter
中的 dep.notify
通知依賴本data
的render watcher
去 update
,update
會把 flushSchedulerQueue
函數傳遞給 nextTick
,render watcher
在 flushSchedulerQueue
函數運行時 watcher.run
再走 diff -> patch
那一套重渲染 re-render
視圖,這個過程當中會從新依賴收集,這個過程是異步的;因此當咱們直接修改了name
以後打印,這時異步的改動尚未被 patch
到視圖上,因此獲取視圖上的DOM
元素仍是原來的內容。setter前
: setter前
爲何還打印原來的是原來內容呢,是由於 nextTick
在被調用的時候把回調挨個push
進callbacks
數組,以後執行的時候也是 for
循環出來挨個執行,因此是相似於隊列這樣一個概念,先入先出;在修改name
以後,觸發把render watcher
填入 schedulerQueue
隊列並把他的執行函數 flushSchedulerQueue
傳遞給 nextTick
,此時callbacks
隊列中已經有了 setter
前函數 了,由於這個 cb
是在 setter
前函數 以後被push
進callbacks
隊列的,那麼先入先出的執行callbacks
中回調的時候先執行 setter
前函數,這時並未執行render watcher
的 watcher.run
,因此打印DOM
元素仍然是原來的內容。setter後
: setter後
這時已經執行完 flushSchedulerQueue
,這時render watcher
已經把改動 patch
到視圖上,因此此時獲取DOM
是改過以後的內容。Promise方式
: 至關於 Promise.then
的方式執行這個函數,此時DOM
已經更改。setTimeout方式
: 最後執行macro task
的任務,此時DOM
已經更改。 注意,在執行 setter前
函數 這個異步任務以前,同步的代碼已經執行完畢,異步的任務都還未執行,全部的 $nextTick
函數也執行完畢,全部回調都被push
進了callbacks
隊列中等待執行,因此在setter前
函數執行的時候,此時callbacks
隊列是這樣的:[setter前
函數,flushSchedulerQueue
,setter後
函數,Promise
方式函數],它是一個micro task
隊列,執行完畢以後執行macro task
、setTimeout
,因此打印出上面的結果。
另外,若是瀏覽器的宏任務隊列裏面有setImmediate
、MessageChannel
、setTimeout/setInterval
各類類型的任務,那麼會按照上面的順序挨個按照添加進event loop
中的順序執行,因此若是瀏覽器支持MessageChannel
, nextTick
執行的是 macroTimerFunc
,那麼若是 macrotask queue
中同時有 nextTick
添加的任務和用戶本身添加的 setTimeout
類型的任務,會優先執行 nextTick
中的任務,由於MessageChannel
的優先級比 setTimeout
的高,setImmediate
同理。
以上部份內容來源與本身複習時的網絡查找,也主要用於我的學習,至關於記事本的存在,暫不列舉連接文章。若是有做者看到,能夠聯繫我將原文連接貼出。