vue中nexttick原理(原始碼分析)
阿新 • • 發佈:2019-02-01
nexttick函式的作用是來延遲一個函式的執行。
結合vue nexttick.js原始碼進行分析:
/* @flow */ /* globals MessageChannel */ import { noop } from 'shared/util' import { handleError } from './error' import { isIOS, isNative } from './env' const callbacks = [] // 回撥函式陣列 let pending = false // 是否正在執行的flag 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). // microtasks和(macro) tasks是非同步延遲的包裹函式,2.4版本之前只用microtasks,但因為其在一些 // 情況下優先順序過高,且可能在一些連續事件中觸發,甚至是同一事件的冒泡間觸發。然而都用(macro) // task在state在重繪前改變也會存在一些問題。所以預設使用microtask,但也會再需要時強制改變為 // (macro) task 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. // 定義(macro) task的延遲實現,setImmediate是最優選,但只在IE中可用。所以在同一迴圈中所有DOM // 事件觸發後,要把回撥推進同一佇列中則使用MessageChannel /* istanbul ignore if */ if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { //如果有setImmediate且是原生程式碼則使用它來延遲 macroTimerFunc = () => { setImmediate(flushCallbacks) } } else if (typeof MessageChannel !== 'undefined' && ( //其次使用MessageChannel isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) } } else { // 最後考慮使用setTimeout /* istanbul ignore next */ macroTimerFunc = () => { setTimeout(flushCallbacks, 0) } } // Determine microtask defer implementation. // 定義microtask延遲的實現 /* istanbul ignore next, $flow-disable-line */ if (typeof Promise !== 'undefined' && isNative(Promise)) { //支援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. // 在一些有問題的UIWebview中,Promise.then沒有完全中斷,但它可能卡在一個狀態:我已經將回調 // 推進佇列中但這個佇列並沒有重新整理。直到瀏覽器做一些其他工作,例如處理一個計時器。所以需要 // 手動呼叫一個空的計數器來“強制”重新整理佇列 if (isIOS) setTimeout(noop) } } else { // 否則使用macroTimerFunc() // 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 }) } // vue原始碼中的nexttick接收兩個引數,要延遲執行的回撥函式(callback),和執行該函式的指定上下文 //(context),如果沒有上下文引數,則會預設為全域性上下文。 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) { /沒有返回context的promise _resolve(ctx) } }) if (!pending) { // 如果當前沒有執行回撥則執行 pending = true if (useMacroTask) { // 預設優先使用microTimerFunc() macroTimerFunc() } else { microTimerFunc() } } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { //如果麼有回撥,且支援promise,返回promise的resolve函式 return new Promise(resolve => { _resolve = resolve }) } }
我們可以看出nexttick將需要延遲的函式放到了一個非同步佇列中執行,setTimeout或Promise等,來起到延遲執行的作用。
很多用途都是用於將函式放到Dom更新後執行,比如在created生命週期中拿不到dom因為還沒渲染掛載到頁面,這時就需要將對dom的操作放到nexttick函式中。那麼為什麼nexttick中的函式能延遲到dom更新完成後呢?
因為採用的是非同步回撥,所有非同步函式都會在同步函式執行完之後在進行呼叫,而DOM的更新在同一事件迴圈中是同步的,所以能在其後執行,當然DOM操作可以放在非同步函式中,但它本身是同步的。
關於非同步函式的呼叫也有些不同,promise是在當前佇列的末尾,setTimeout是在新佇列的開頭,所以同為非同步函式,promise要比setTimeout先呼叫,即使setTimeout寫在前面。所有的同類型非同步函式也是按定義順序執行的。
本文屬於個人理解,如有錯誤,希望更正。