Vue 的特色之一就是響應式,但數據更新時,DOM 並不會當即更新。當咱們有一個業務場景,須要在 DOM 更新以後再執行一段代碼時,能夠藉助nextTick
實現。如下是來自官方文檔的介紹:html
將回調延遲到下次 DOM 更新循環以後執行。在修改數據以後當即使用它,而後等待 DOM 更新。
具體的使用場景和底層代碼實如今後面的段落說明和解釋。api
Vue.nextTick([callback, context])
與 vm.$nextTick([callback])
數組
前者是全局方法,能夠顯式指定執行上下文,然後者是實例方法,執行時自動綁定this
到當前實例上。瀏覽器
此外,在 2.1.0 版本還新增了不傳入回調的使用方式,這種調用會返回一個 Promise,在 then 的回調執行目標操做便可,如vm.$nextTick().then(cb)
。app
如下是一個nextTick
使用例子:函數
<div id="app"> <button @click="add">add</button> {{count}} <ul ref="ul"> <li v-for="item in list"> {{item}} </li> </ul> </div>
new Vue({ el: "#app", data: { count: 0, list: [] }, methods: { add() { this.count += 1; this.list.push(1); let li = this.$refs.ul.querySelectorAll("li"); li.forEach(item => { item.style.color = "red"; }); } } });
以上的代碼,指望在每次新增一個列表項時都使得列表項的字體是紅色的,但實際上新增的列表項字體還是黑色的。儘管data
已經更新,但新增的 li 元素並不當即插入到 DOM 中。若是但願在 DOM 更新後再更新樣式,能夠在nextTick
的回調中執行更新樣式的操做。oop
new Vue({ el: "#app", data: { count: 0, list: [] }, methods: { add() { this.count += 1; this.list.push(1); this.$nextTick(() => { let li = this.$refs.ul.querySelectorAll("li"); li.forEach(item => { item.style.color = "red"; }); }); } } });
數據更新時,並不會當即更新 DOM。若是在更新數據以後的代碼執行另外一段代碼,有可能達不到預想效果。將視圖更新後的操做放在nextTick
的回調中執行,其底層經過微任務的方式執行回調,能夠保證 DOM 更新後才執行代碼。post
在/src/core/instance/index.js
,執行方法renderMixin(Vue)
爲Vue.prototype
添加了$nextTick
方法。實際在Vue.prototype.$nextTick
中,執行了nextTick(fn, this)
,這也是vm.$nextTick( [callback] )
自動綁定this
到執行上下文的緣由。字體
nextTick
函數在/scr/core/util/next-tick.js
聲明。在next-tick.js
內,使用數組callbacks
保存回調函數,pending
表示當前狀態,使用函數flushCallbacks
來執行回調隊列。在該方法內,先經過slice(0)
保存了回調隊列的一個副本,經過設置callbacks.length = 0
清空回調隊列,最後使用循環執行在副本里的全部函數。this
const callbacks = []; let pending = false; function flushCallbacks() { pending = false; const copies = callbacks.slice(0); callbacks.length = 0; for (let i = 0; i < copies.length; i++) { copies[i](); } }
接着定義函數marcoTimerFunc
、microTimerFunc
。
先判斷是否支持setImmediate
,若是支持,使用setImmediate
執行回調隊列;若是不支持,判斷是否支持MessageChannel
,支持時,在port1
監聽message
,將flushCallbacks
做爲回調;若是仍不支持MessageChannel
,使用setTimeout(flushCallbacks, 0)
執行回調隊列。無論使用哪一種方式,macroTimerFunc
最終目的都是在一個宏任務裏執行回調隊列。
if (typeof setImmediate !== "undefined" && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks); }; } else if ( typeof MessageChannel !== "undefined" && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === "[object MessageChannelConstructor]") ) { const channel = new MessageChannel(); const port = channel.port2; channel.port1.onmessage = flushCallbacks; macroTimerFunc = () => { port.postMessage(1); }; } else { /* istanbul ignore next */ macroTimerFunc = () => { setTimeout(flushCallbacks, 0); }; }
而後判斷是否支持Promise
,支持時,新建一個狀態爲resolved
的Promise
對象,並在then
回調裏執行回調隊列,如此,便在一個微任務中執行回調,在 IOS 的 UIWebViews 組件中,儘管能建立一個微任務,但這個隊列並不會執行,除非瀏覽器須要執行其餘任務;因此使用setTimeout
添加一個不執行任何操做的回調,使得微任務隊列被執行。若是不支持Promise
,使用降級方案,將microTimerFunc
指向macroTimerFunc
。
if (typeof Promise !== "undefined" && isNative(Promise)) { const p = Promise.resolve(); microTimerFunc = () => { p.then(flushCallbacks); // in problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) setTimeout(noop); }; } else { // fallback to macro microTimerFunc = macroTimerFunc; }
在函數nextTick
內,先將函數cb
使用箭頭函數包裝起來並添加到回調隊列callbacks
,轉入的回調cb
會在callbacks
被遍歷執行的時候執行。若是沒有傳入cb
,則是形如this.$nextTick().then(cb)
的使用方式,因此要返回一個fulfilled
的 Promise,在箭頭函數內則須要執行resolved
,令返回的 Promise 狀態變爲fulfilled
。接着判斷當前是否正在執行回調,若是不是,將pengding
設置爲真。判斷回調執行是宏任務仍是微任務,分別經過marcoTimerFunc
、microTimerFunc
來觸發回調隊列。最後,若是,沒有傳入cb
,則須要建立一個Promise
實例並返回以支持鏈式調用,而且將_resolve
指向返回 Promise 的resolve
函數。
export function nextTick(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; if (useMacroTask) { macroTimerFunc(); } else { microTimerFunc(); } } // $flow-disable-line if (!cb && typeof Promise !== "undefined") { return new Promise(resolve => { _resolve = resolve; }); } }
而全局方法Vue.nextTick
在/src/core/global-api/index.js
中聲明,是對函數nextTick
的引用,因此使用時能夠顯式指定執行上下文。
Vue.nextTick = nextTick;
本文關於nextTick
的使用場景和源碼作了簡單的介紹,若是想深刻了解這部分的知識,能夠去了解一下微任務mircotask
和宏任務marcotask
。