首先來了解一下JS的運行機制。html
JS執行是單線程的,它是基於事件循環的。vue
這裏主線程的執行過程就是一個tick
,而全部的異步結果都是經過任務隊列來調度。Event Loop
分爲宏任務和微任務,不管是執行宏任務仍是微任務,完成後都會進入到一下tick
,並在兩個tick
之間進行UI渲染。react
因爲Vue DOM更新是異步執行的,即修改數據時,視圖不會當即更新,而是會監聽數據變化,並緩存在同一事件循環中,等同一數據循環中的全部數據變化完成以後,再統一進行視圖更新。爲了確保獲得更新後的DOM,因此設置了 Vue.nextTick()
方法。git
是Vue的核心方法之一,官方文檔解釋以下:github
在下次DOM更新循環結束以後執行延遲迴調。在修改數據以後當即使用這個方法,獲取更新後的DOM。
先簡單介紹下MutationObserver
:MO是HTML5中的API,是一個用於監視DOM變更的接口,它能夠監聽一個DOM對象上發生的子節點刪除、屬性修改、文本內容修改等。數組
調用過程是要先給它綁定回調,獲得MO實例,這個回調會在MO實例監聽到變更時觸發。這裏MO的回調是放在microtask
中執行的。promise
// 建立MO實例 const observer = new MutationObserver(callback) const textNode = '想要監聽的Don節點' observer.observe(textNode, { characterData: true // 說明監聽文本內容的修改 })
nextTick
的實現單獨有一個JS文件來維護它,在src/core/util/next-tick.js
中。瀏覽器
nextTick
源碼主要分爲兩塊:能力檢測和根據能力檢測以不一樣方式執行回調隊列。緩存
因爲宏任務耗費的時間是大於微任務的,因此在瀏覽器支持的狀況下,優先使用微任務。若是瀏覽器不支持微任務,再使用宏任務。安全
// 空函數,可用做函數佔位符 import { noop } from 'shared/util' // 錯誤處理函數 import { handleError } from './error' // 是不是IE、IOS、內置函數 import { isIE, isIOS, isNative } from './env' // 使用 MicroTask 的標識符,這裏是由於火狐在<=53時 沒法觸發微任務,在modules/events.js文件中引用進行安全排除 export let isUsingMicroTask = false // 用來存儲全部須要執行的回調函數 const callbacks = [] // 用來標誌是否正在執行回調函數 let pending = false // 對callbacks進行遍歷,而後執行相應的回調函數 function flushCallbacks () { pending = false // 這裏拷貝的緣由是: // 有的cb 執行過程當中又會往callbacks中加入內容 // 好比 $nextTick的回調函數裏還有$nextTick // 後者的應該放到下一輪的nextTick 中執行 // 因此拷貝一份當前的,遍歷執行完當前的便可,避免無休止的執行下去 const copies = callbcks.slice(0) callbacks.length = 0 for(let i = 0; i < copies.length; i++) { copies[i]() } } let timerFunc // 異步執行函數 用於異步延遲調用 flushCallbacks 函數 // 在2.5中,咱們使用(宏)任務(與微任務結合使用)。 // 然而,當狀態在從新繪製以前發生變化時,就會出現一些微妙的問題 // (例如#6813,out-in轉換)。 // 一樣,在事件處理程序中使用(宏)任務會致使一些奇怪的行爲 // 所以,咱們如今再次在任何地方使用微任務。 // 優先使用 Promise if(typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // IOS 的UIWebView, Promise.then 回調被推入 microTask 隊列,可是隊列可能不會如期執行 // 所以,添加一個空計時器強制執行 microTask if(isIOS) setTimeout(noop) } isUsingMicroTask = true } else if(!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString === '[object MutationObserverConstructor]')) { // 當 原生Promise 不可用時,使用 原生MutationObserver // e.g. PhantomJS, iOS7, Android 4.4 let counter = 1 // 建立MO實例,監聽到DOM變更後會執行回調flushCallbacks const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true // 設置true 表示觀察目標的改變 }) // 每次執行timerFunc 都會讓文本節點的內容在 0/1之間切換 // 切換以後將新值複製到 MO 觀測的文本節點上 // 節點內容變化會觸發回調 timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) // 觸發回調 } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { timerFunc = () => { setTimeout(flushCallbacks, 0) } }
延遲調用優先級以下:
Promise > MutationObserver > setImmediate > setTimeout
export function nextTick(cb? Function, ctx: Object) { let _resolve // cb 回調函數會統一處理壓入callbacks數組 callbacks.push(() => { if(cb) { try { cb.call(ctx) } catch(e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) // pending 爲false 說明本輪事件循環中沒有執行過timerFunc() if(!pending) { pending = true timerFunc() } // 當不傳入 cb 參數時,提供一個promise化的調用 // 如nextTick().then(() => {}) // 當_resolve執行時,就會跳轉到then邏輯中 if(!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
next-tick.js
對外暴露了nextTick
這一個參數,因此每次調用Vue.nextTick
時會執行:
cb
壓入callbacks
數組timerFunc
函數,延遲調用 flushCallbacks
函數callbacks
數組中的全部函數這裏的 callbacks
沒有直接在 nextTick
中執行回調函數的緣由是保證在同一個 tick
內屢次執行nextTick
,不會開啓多個異步任務,而是把這些異步任務都壓成一個同步任務,在下一個 tick
執行完畢。
noop
的定義以下
/** * Perform no operation. * Stubbing args to make Flow happy without leaving useless transpiled code * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/). */ export function noop (a?: any, b?: any, c?: any) {}
語法:Vue.nextTick([callback, context])
參數:
{Function} [callback]
:回調函數,不傳時提供promise調用{Object} [context]
:回調函數執行的上下文環境,不傳默認是自動綁定到調用它的實例上。//改變數據 vm.message = 'changed' //想要當即使用更新後的DOM。這樣不行,由於設置message後DOM尚未更新 console.log(vm.$el.textContent) // 並不會獲得'changed' //這樣能夠,nextTick裏面的代碼會在DOM更新後執行 Vue.nextTick(function(){ // DOM 更新了 //能夠獲得'changed' console.log(vm.$el.textContent) }) // 做爲一個 Promise 使用 即不傳回調 Vue.nextTick() .then(function () { // DOM 更新了 })
Vue實例方法vm.$nextTick
作了進一步封裝,把context參數設置成當前Vue實例。
使用Vue.nextTick()
是爲了能夠獲取更新後的DOM 。
觸發時機:在同一事件循環中的數據變化後,DOM完成更新,當即執行Vue.nextTick()
的回調。
同一事件循環中的代碼執行完畢 -> DOM 更新 -> nextTick callback觸發
應用場景:
created()
鉤子函數進行的DOM操做必定要放在Vue.nextTick()
的回調函數中。緣由:是created()
鉤子函數執行時DOM其實並未進行渲染。
Vue.nextTick()
的回調函數中。緣由:Vue異步執行DOM更新,只要觀察到數據變化,Vue將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據改變,若是同一個watcher被屢次觸發,只會被推入到隊列中一次。
2.6 版本優先使用 microtask 做爲異步延遲包裝器,且寫法相對簡單。而2.5 版本中,nextTick 的實現是 microTimerFunc、macroTimerFunc 組合實現的,延遲調用優先級是:Promise > setImmediate > MessageChannel > setTimeout,具體見源碼。
2.5 版本在重繪以前狀態改變時會有小問題(如 #6813)。此外,在事件處理程序中使用 macrotask 會致使一些沒法規避的奇怪行爲(如 #7109,#7153等)。
microtask 在某些狀況下也是會有問題的,由於 microtask 優先級比較高,事件會在順序事件(如#4521,#6690 有變通方法)之間甚至在同一事件的冒泡過程當中觸發(#6566)。
參考: