<h1>Vue.nextTick淺析</h1> <p>Vue的特色之一就是響應式,但數據更新時,DOM並不會當即更新。當咱們有一個業務場景,須要在DOM更新以後再執行一段代碼時,能夠藉助<code>nextTick</code>實現。如下是來自官方文檔的介紹:</p> <blockquote>將回調延遲到下次 DOM 更新循環以後執行。在修改數據以後當即使用它,而後等待 DOM 更新。</blockquote> <p>具體的使用場景和底層代碼實如今後面的段落說明和解釋。</p> <h2>用途</h2> <p><code>Vue.nextTick( [callback, context] )</code> 與 <code>vm.$nextTick( [callback] )</code></p> <p>前者是全局方法,能夠顯式指定執行上下文,然後者是實例方法,執行時自動綁定<code>this</code>到當前實例上。</p> <p>如下是一個<code>nextTick</code>使用例子:</p> ```<div id="app"> <button @click="add">add</button> {{count}} <ul ref="ul"> <li v-for="item in list"> {{item}} </li> </ul> </div> ```segmentfault
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'; }) } } })
<p>以上的代碼,指望在每次新增一個列表項時都使得列表項的字體是紅色的,但實際上新增的列表項字體還是黑色的。儘管<code>data</code>已經更新,但新增的li元素並不當即插入到DOM中。若是但願在DOM更新後再更新樣式,能夠在<code>nextTick</code>的回調中執行更新樣式的操做。</p>api
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'; }) }) } } })
<h3>解釋</h3> <p>數據更新時,並不會當即更新DOM。若是在更新數據以後的代碼執行另外一段代碼,有可能達不到預想效果。將視圖更新後的操做放在<code>nextTick</code>的回調中執行,其底層經過微任務的方式執行回調,能夠保證DOM更新後才執行代碼。</p> <h2>源碼</h2> <p>在<code>/src/core/instance/index.js</code>,執行方法<code>renderMixin(Vue)</code>爲<code>Vue.prototype</code>添加了<code>$nextTick</code>方法。實際在<code>Vue.prototype.$nextTick</code>中,執行了<code>nextTick(fn, this)</code>,這也是<code>vm.$nextTick( [callback] )</code>自動綁定<code>this</code>到執行上下文的緣由。</p> <p><code>nextTick</code>函數在<code>/scr/core/util/next-tick.js</code>聲明。在<code>next-tick.js</code>內,使用數組<code>callbacks</code>保存回調函數,<code>pending</code>表示當前狀態,使用函數<code>flushCallbacks</code>來執行回調隊列。在該方法內,先經過<code>slice(0)</code>保存了回調隊列的一個副本,經過設置<code>callbacks.length = 0</code>清空回調隊列,最後使用循環執行在副本里的全部函數。</p>數組
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]() } }
<p>接着定義函數<code>marcoTimerFunc</code>、<code>microTimerFunc</code>。</p> <p>先判斷是否支持<code>setImmediate</code>,若是支持,使用<code>setImmediate</code>執行回調隊列;若是不支持,判斷是否支持<code>MessageChannel</code>,支持時,在<code>port1</code>監聽<code>message</code>,將<code>flushCallbacks</code>做爲回調;若是仍不支持<code>MessageChannel</code>,使用<code>setTimeout(flushCallbacks, 0)</code>執行回調隊列。無論使用哪一種方式,<code>macroTimerFunc</code>最終目的都是在一個宏任務裏執行回調隊列。</p>瀏覽器
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) } }
<p>而後判斷是否支持<code>Promise</code>,支持時,新建一個狀態爲<code>resolved</code>的<code>Promise</code>對象,並在<code>then</code>回調裏執行回調隊列,如此,便在一個微任務中執行回調,在IOS的UIWebViews組件中,儘管能建立一個微任務,但這個隊列並不會執行,除非瀏覽器須要執行其餘任務;因此使用<code>setTimeout</code>添加一個不執行任何操做的回調,使得微任務隊列被執行。若是不支持<code>Promise</code>,使用降級方案,將<code>microTimerFunc</code>指向<code>macroTimerFunc</code>。</p>app
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 }
<p>在函數<code>nextTick</code>內,先將函數<code>cb</code>使用箭頭函數包裝起來並添加到回調隊列<code>callbacks</code>。接着判斷當前是否正在執行回調,若是不是,將<code>pengding</code>設置爲真。判斷回調執行是宏任務仍是微任務,分別經過<code>marcoTimerFunc</code>、<code>microTimerFunc</code>來觸發回調隊列。最後返回一個<code>Promise</code>實例以支持鏈式調用。</p>函數
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 }) } }
<p>而全局方法<code>Vue.nextTick</code>在<code>/src/core/global-api/index.js</code>中聲明,是對函數<code>nextTick</code>的引用,因此使用時能夠顯示指定執行上下文。</p>oop
Vue.nextTick = nextTick
<h2>小結</h2> <p>本文關於<code>nextTick</code>的使用場景和源碼作了簡單的介紹,若是想深刻了解這部分的知識,能夠去了解一下微任務<code>mircotask</code>和宏任務<code>marcotask</code>。</p>post