咱們知道vue中有一個api。Vue.nextTick( [callback, context] )
他的做用是在下次 DOM 更新循環結束以後執行延遲迴調。在修改數據以後當即使用這個方法,獲取更新後的 DOM。那麼這個api是怎麼實現的呢?你確定也有些疑問或者好奇。下面就是個人探索,分享給你們,也歡迎你們到github上和我進行討論哈~~vue
首先貼一下vue的源碼,而後咱們再一步步的分析ios
/* @flow */ /* globals MessageChannel */ import { noop } from 'shared/util' import { handleError } from './error' import { isIOS, isNative } from './env' 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]() } } // Here we have async deferring wrappers using both microtasks and (macro) tasks. // In < 2.4 we used microtasks everywhere, but there are some scenarios where // microtasks have too high a priority and fire in between supposedly // sequential events (e.g. #4521, #6690) or even between bubbling of the same // event (#6566). However, using (macro) tasks everywhere also has subtle problems // when state is changed right before repaint (e.g. #6813, out-in transitions). // Here we use microtask by default, but expose a way to force (macro) task when // needed (e.g. in event handlers attached by v-on). let microTimerFunc let macroTimerFunc let useMacroTask = false // 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 */ 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) } } // Determine microtask defer implementation. /* istanbul ignore next, $flow-disable-line */ 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 } /** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */ export function withMacroTask (fn: Function): Function { return fn._withTask || (fn._withTask = function () { useMacroTask = true const res = fn.apply(null, arguments) useMacroTask = false return res }) } 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 }) } }
這麼多代碼,可能猛的一看,可能有點懵,沒關係,咱們一步一步抽出枝幹。首先咱們看一下這個js文件裏的nextTick的定義git
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 }) } }
我將代碼精簡一下。以下所示,下面的代碼估計就比較容易看懂了。把cb函數放到會掉隊列裏去,若是支持macroTask,則利用macroTask在下一個事件循環中執行這些異步的任務,若是不支持macroTask,那就利用microTask在下一個事件循環中執行這些異步任務。github
export function nextTick (cb?: Function, ctx?: Object) { callbacks.push(cb) if (useMacroTask) { macroTimerFunc() } else { microTimerFunc() } }
這裏再次普及一下js的event loop的相關知識,js中的兩個任務隊列 :macrotasks、microtasksapi
macrotasks: script(一個js文件),setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe, MutationObserverpromise
執行過程:
1.js引擎從macrotask隊列中取一個任務執行
2.而後將microtask隊列中的全部任務依次執行完
3.再次從macrotask隊列中取一個任務執行
4.而後再次將microtask隊列中全部任務依次執行完
……
循環往復瀏覽器
那麼咱們再看咱們精簡掉的代碼都是幹什麼的呢?咱們往異步隊列裏放回調函數的時候,咱們並非直接放回調函數,而是包裝了一個函數,在這個函數裏調用cb,而且用try catch包裹了一下。這是由於cb在運行時出錯,咱們不try catch這個錯誤的話,會致使整個程序崩潰掉。 咱們還精簡掉了以下代碼app
if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) }
這段代碼是幹嗎的呢?也就是說咱們用nextTick的時候,還能夠有promise的寫法。若是沒有向nextTick中傳入cb,而且瀏覽器支持Promise的話,咱們的nextTick返回的將是一個Promise。因此,nextTick的寫法也能夠是以下這樣的異步
nextTick().then(()=>{console.log("XXXXX")})
vue源碼裏關於nextTick的封裝的思路,也給咱們一些很是有益的啓示,就是咱們平時在封裝函數的時候,要想同時指出回調和promise的話,就能夠借鑑vue中的思路。async
大體的思路咱們已經捋順了。可是爲何執行macroTimerFunc或者microTimerFunc就會在下一個tick執行咱們的回調隊列呢?下面咱們來分析一下這兩個函數的定義。首先咱們分析macroTimerFunc
let 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) } }
從上邊的代碼能夠看出,若是瀏覽器支持setImmediate,咱們就用setImmediate,若是瀏覽器支持MessageChannel,咱們就用MessageChannel的異步特性,若是二者都不支持,咱們就降價到setTimeout
,用setTimeout來把callbacks中的任務在下一個tick中執行
macroTimerFunc = () => { setTimeout(flushCallbacks, 0) }
分析完macroTimerFunc,下面咱們開始分析microTimerFunc,我把vue源碼中關於microTimerFunc的定義稍微精簡一下
let microTimerFunc; if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() microTimerFunc = () => { p.then(flushCallbacks) } } else { // fallback to macro microTimerFunc = macroTimerFunc }
從上邊精簡以後的代碼,咱們能夠看到microTimerFunc的實現思路。若是支持瀏覽器支持promise,就用promise實現。若是不支持,就下降到用macroTimerFunc
over,總體邏輯就是這樣。。看着嚇人,掰開了以後好好分析一下,仍是挺簡單的。