nextTick是Vue的一個核心功能,在Vue內部實現中也常常用到nextTick。可是,不少新手不理解nextTick的原理,甚至不清楚nextTick的做用。vue
那麼,咱們就先來看看nextTick是什麼。瀏覽器
看看官方文檔的描述:dom
在下次 DOM 更新循環結束以後執行延遲迴調。在修改數據以後當即使用這個方法,獲取更新後的 DOM。異步
再看看官方示例:ide
// 修改數據 vm.msg = 'Hello' // DOM 尚未更新 Vue.nextTick(function () { // DOM 更新了 }) // 做爲一個 Promise 使用 (2.1.0 起新增,詳見接下來的提示) Vue.nextTick() .then(function () { // DOM 更新了 })
2.1.0 起新增:若是沒有提供回調且在支持 Promise 的環境中,則返回一個 Promise。請注意 Vue 不自帶 Promise 的 polyfill,因此若是你的目標瀏覽器不原生支持 Promise (IE:大家都看我幹嗎),你得本身提供 polyfill。函數
能夠看到,nextTick主要功能就是改變數據後讓回調函數做用於dom更新後。不少人一看到這裏就懵逼了,爲何須要在dom更新後再執行回調函數,我修改了數據後,不是dom自動就更新了嗎?oop
這個和JS中的Event Loop有關,網上教程不可勝數,在此就再也不贅述了。建議明白Event Loop後再繼續向下閱讀本文。源碼分析
舉個實際的例子:post
咱們有個帶有分頁器的表格,每次翻頁須要選中第一項。正常狀況下,咱們想的是點擊翻頁器,向後臺獲取數據,更新表格數據,操縱表格API選中第一項。測試
可是,你會發現,表格數據是更新了,可是並無選中第一項。由於,你選中第一項時,雖然數據更新了,可是DOM並無更新。此時,你可使用nextTick,在DOM更新後再操縱表格第一項的選中。
那麼,nextTick到底作了什麼了才能實如今DOM更新後執行回調函數?
nextTick的源碼位於src/core/util/next-tick.js,總計118行,十分的短小精悍,十分適合初次閱讀源碼的同窗。
nextTick源碼主要分爲兩塊:
1.能力檢測
2.根據能力檢測以不一樣方式執行回調隊列
這一塊其實很簡單,衆所周知,Event Loop分爲宏任務(macro task)以及微任務( micro task),無論執行宏任務仍是微任務,完成後都會進入下一個tick,並在兩個tick之間執行UI渲染。
可是,宏任務耗費的時間是大於微任務的,因此在瀏覽器支持的狀況下,優先使用微任務。若是瀏覽器不支持微任務,使用宏任務;可是,各類宏任務之間也有效率的不一樣,須要根據瀏覽器的支持狀況,使用不一樣的宏任務。
nextTick在能力檢測這一塊,就是遵循的這種思想。
// Determine (macro) task defer implementation. // Technically setImmediate should be the ideal choice, but it's only available // in IE. The only polyfill that consistently queues the callback after all DOM // events triggered in the same loop is by using MessageChannel. /* istanbul ignore if */ // 若是瀏覽器不支持Promise,使用宏任務來執行nextTick回調函數隊列 // 能力檢測,測試瀏覽器是否支持原生的setImmediate(setImmediate只在IE中有效) if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // 若是支持,宏任務( macro task)使用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 */ // 都不支持的狀況下,使用setTimeout macroTimerFunc = () => { setTimeout(flushCallbacks, 0) } }
首先,檢測瀏覽器是否支持setImmediate,不支持就使用MessageChannel,再不支持只能使用效率最差可是兼容性最好的setTimeout了。
以後,檢測瀏覽器是否支持Promise,若是支持,則使用Promise來執行回調函數隊列,畢竟微任務速度大於宏任務。若是不支持的話,就只能使用宏任務來執行回調函數隊列。
執行回調函數隊列的代碼恰好在一頭一尾
// 回調函數隊列 const callbacks = [] // 異步鎖 let pending = false // 執行回調函數 function flushCallbacks () { // 重置異步鎖 pending = false // 防止出現nextTick中包含nextTick時出現問題,在執行回調函數隊列前,提早複製備份,清空回調函數隊列 const copies = callbacks.slice(0) callbacks.length = 0 // 執行回調函數隊列 for (let i = 0; i < copies.length; i++) { copies[i]() } } ... // 咱們調用的nextTick函數 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 // 2.1.0新增,若是沒有提供回調,而且支持Promise,返回一個Promise if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
整體流程就是,接收回調函數,將回調函數推入回調函數隊列中。
同時,在接收第一個回調函數時,執行能力檢測中對應的異步方法(異步方法中調用了回調函數隊列)。
如何保證只在接收第一個回調函數時執行異步方法?
nextTick源碼中使用了一個異步鎖的概念,即接收第一個回調函數時,先關上鎖,執行異步方法。此時,瀏覽器處於等待執行完同步代碼就執行異步代碼的狀況。
打個比喻:至關於一羣旅客準備上車,當第一個旅客上車的時候,車開始發動,準備出發,等到全部旅客都上車後,就能夠正式開車了。
固然執行flushCallbacks函數時有個難以理解的點,即:爲何須要備份回調函數隊列?執行的也是備份的回調函數隊列?
由於,會出現這麼一種狀況:nextTick套用nextTick。若是flushCallbacks不作特殊處理,直接循環執行回調函數,會致使裏面nextTick中的回調函數會進入回調隊列。這就至關於,下一個班車的旅客上了上一個班車。
說了這麼多,咱們來實現一個簡單的nextTick:
let callbacks = [] let pending = false function nextTick (cb) { callbacks.push(cb) if (!pending) { pending = true setTimeout(flushCallback, 0) } } function flushCallback () { pending = false let copies = callbacks.slice() callbacks.length = 0 copies.forEach(copy => { copy() }) }
能夠看到,在簡易版的nextTick中,經過nextTick接收回調函數,經過setTimeout來異步執行回調函數。經過這種方式,能夠實如今下一個tick中執行回調函數,即在UI從新渲染後執行回調函數。