摘要:本文經過結合官方文檔、源碼和其餘文章整理後,對Vue的nextTick作深刻解析。理解本文最好有瀏覽器事件循環的基礎,建議先閱讀上文《事件循環Event loop究竟是什麼》。 html
實際上在弄清楚瀏覽器的事件循環後,Vue的nextTick就很是好理解了,它就是利用了事件循環的機制。咱們首先來看看nextTick在Vue官方文檔中是如何描述的:vue
Vue在更新DOM時是異步執行的,只要偵聽到數據變化,Vue將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。若是同一個watcher被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和DOM操做是很是重要的。而後,在下一個事件循環「tick」中,Vue刷新隊列並執行實際(已去重的)工做。Vue在內部對異步隊列嘗試使用原生的Promise.then、MutationObserver和setImmediate,若是執行環境不支持,則會採用setTimeout(fn,0)代替。
當刷新隊列時,組件會在下一個事件循環「tick」中更新。多數狀況咱們不須要關心這個過程,可是若是你想基於更新後的 DOM 狀態來作點什麼,這就可能會有些棘手。雖然 Vue.js 一般鼓勵開發人員使用「數據驅動」的方式思考,避免直接接觸 DOM,可是有時咱們必需要這麼作。爲了在數據變化以後等待 Vue 完成更新 DOM,能夠在數據變化以後當即使用 Vue.nextTick(callback)。 express
簡單來講,Vue爲了保證數據屢次變化操做DOM更新的性能,採用了異步更新DOM的機制,且同一事件循環中同一個數據屢次修改只會取最後一次修改結果。而這種方式產生一個問題,開發人員沒法經過同步代碼獲取數據更新後的DOM狀態,因此Vue就提供了Vue.nextTick方法,經過這個方法的回調就能獲取當前DOM更新後的狀態。 數組
但只看官方解釋可能仍是會有些疑問,好比描述中說到的下一個事件循環「tick」是什麼意思?爲何會是下一個事件循環?接下來咱們看源碼究竟是怎麼實現的。瀏覽器
Vue.nextTick的源碼部分主要分爲Watcher部分和NextTick部分,因爲Watcher部分的源碼在前文《深刻解析vue響應式原理》中,已經詳細分析過了,因此這裏關於Watcher的源碼就直接分析觸發update以後的部分。 異步
update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } } }
function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id queue.sort((a, b) => a.id - b.id) for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() // in dev build, check and stop circular updates. if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn( 'You may have an infinite update loop ' + ( watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.` ), watcher.vm ) break } } } // keep copies of post queues before resetting state const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() // call component updated and activated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush') } }
根據前文《深刻解析vue響應式原理》能夠知道,數據變化後會首先觸發關聯Dep的notify方法,而後會調用全部依賴該數據的Watcher.update方法。接下來的步驟總結以下: async
- update又調用了queueWatcher方法;
- queueWatcher方法中使用靜態全局Watcher數組queue來保存當前的watcher,而且若是Watcher重複,只會保留最新的Watcher;
- 而後是flushSchedulerQueue方法,簡單來講,flushSchedulerQueue方法中主要就是遍歷queue數組,依次執行了全部的Watcher.run,操做DOM更新;
- 但flushSchedulerQueue並不會當即執行,而是做爲nextTick參數進入下一層。
重點來到了nextTick這一層。oop
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 timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
let timerFunc if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) } }
function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
nextTick代碼流程總結以下:post
- 結合前面代碼分析來看,遍歷Watcher執行DOM更新的方法傳入了nextTick,在nextTick中被添加到了callbacks數組,隨後執行了timerFunc方法;
- timerFunc方法使用了flushCallbacks方法,flushCallbacks執行了flushSchedulerQueue方法,即執行Watcher關聯的DOM更新。
- 而timerFunc是根據瀏覽器支持狀況,將flushCallbacks(DOM更新操做)做爲參數傳遞給Promise.then、MutationObserver、setImmediate或setTimeout(fn,0)。
到這裏咱們明白了,原來在Vue中數據變動觸發DOM更新操做也是使用了nextTick來實現異步執行的,而Vue提供給開發者使用的nextTick是同一個nextTick。因此官方文檔強調了要在數據變化以後當即使用 Vue.nextTick(callback),這樣就能保證callback是插入隊列裏DOM更新操做的後面,並在同一個事件循環中按順序完成,由於開發者插入的callback在隊尾,那麼始終是在DOM操做後當即執行。 性能
而針對官方文檔「在下一個事件循環"tick"中,Vue刷新隊列並執行實際(已去重的)工做」的描述我以爲是不夠嚴謹的,緣由在於,根據瀏覽器的支持狀況,結合瀏覽器事件循環宏任務和微任務的概念,nextTick使用的是Promise.then或MutationObserver,那就應該是和script(總體代碼)是同一個事件循環;當使用的是setImmediate或setTimeout(fn,0)),那纔在下一個事件循環。
同時,聰明的你或許已經想到了,那按這個原理實際我不須要使用nextTick好像也能夠達到一樣的效果,好比使用setTimeout(fn,0),那咱們直接用一個例子來看一下吧。
<template> <div class="box">{{msg}}</div> </template> <script> export default { name: 'index', data () { return { msg: 'hello' } }, mounted () { this.msg = 'world' let box = document.getElementsByClassName('box')[0] setTimeout(() => { console.log(box.innerHTML) // world }) } }
結果確實符合咱們的想象,不過仔細分析一下,雖然能達到一樣的效果,但跟nextTick有點細微差別的。這個差別就在於,若是使用nextTick是能保證DOM更新操做和callback是放到同一種任務(宏/微任務)隊列來執行的,但使用setTimeout(fn,0)就極可能跟DOM更新操做沒有在同一個任務隊列,而不在同一事件循環執行,不過這細微差別目前還沒發現有什麼問題,反正是能夠正確獲取DOM更新後狀態的。