基於源碼分析Vue的nextTick

摘要:本文經過結合官方文檔、源碼和其餘文章整理後,對Vue的nextTick作深刻解析。理解本文最好有瀏覽器事件循環的基礎,建議先閱讀上文《事件循環Event loop究竟是什麼》。 html

1、官方定義


實際上在弄清楚瀏覽器的事件循環後,Vue的nextTick就很是好理解了,它就是利用了事件循環的機制。咱們首先來看看nextTick在Vue官方文檔中是如何描述的:vue

Vue在更新DOM時是異步執行的,只要偵聽到數據變化,Vue將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。若是同一個watcher被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和DOM操做是很是重要的。而後,在下一個事件循環「tick」中,Vue刷新隊列並執行實際(已去重的)工做。Vue在內部對異步隊列嘗試使用原生的Promise.then、MutationObserversetImmediate,若是執行環境不支持,則會採用setTimeout(fn,0)代替。
當刷新隊列時,組件會在下一個事件循環「tick」中更新。多數狀況咱們不須要關心這個過程,可是若是你想基於更新後的 DOM 狀態來作點什麼,這就可能會有些棘手。雖然 Vue.js 一般鼓勵開發人員使用「數據驅動」的方式思考,避免直接接觸 DOM,可是有時咱們必需要這麼作。爲了在數據變化以後等待 Vue 完成更新 DOM,能夠在數據變化以後當即使用 Vue.nextTick(callback)。 express

簡單來講,Vue爲了保證數據屢次變化操做DOM更新的性能,採用了異步更新DOM的機制,且同一事件循環中同一個數據屢次修改只會取最後一次修改結果。而這種方式產生一個問題,開發人員沒法經過同步代碼獲取數據更新後的DOM狀態,因此Vue就提供了Vue.nextTick方法,經過這個方法的回調就能獲取當前DOM更新後的狀態。 數組

但只看官方解釋可能仍是會有些疑問,好比描述中說到的下一個事件循環「tick」是什麼意思?爲何會是下一個事件循環?接下來咱們看源碼究竟是怎麼實現的。瀏覽器

2、源碼解析


Vue.nextTick的源碼部分主要分爲Watcher部分和NextTick部分,因爲Watcher部分的源碼在前文《深刻解析vue響應式原理》中,已經詳細分析過了,因此這裏關於Watcher的源碼就直接分析觸發update以後的部分。 異步

update

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

queueWatcher

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)
    }
  }
}

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

  1. update又調用了queueWatcher方法;
  2. queueWatcher方法中使用靜態全局Watcher數組queue來保存當前的watcher,而且若是Watcher重複,只會保留最新的Watcher;
  3. 而後是flushSchedulerQueue方法,簡單來講,flushSchedulerQueue方法中主要就是遍歷queue數組,依次執行了全部的Watcher.run,操做DOM更新;
  4. 但flushSchedulerQueue並不會當即執行,而是做爲nextTick參數進入下一層。

重點來到了nextTick這一層。oop

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
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

timerFunc

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)
  }
}

flushCallbacks

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

nextTick代碼流程總結以下:post

  1. 結合前面代碼分析來看,遍歷Watcher執行DOM更新的方法傳入了nextTick,在nextTick中被添加到了callbacks數組,隨後執行了timerFunc方法;
  2. timerFunc方法使用了flushCallbacks方法,flushCallbacks執行了flushSchedulerQueue方法,即執行Watcher關聯的DOM更新。
  3. 而timerFunc是根據瀏覽器支持狀況,將flushCallbacks(DOM更新操做)做爲參數傳遞給Promise.then、MutationObserver、setImmediatesetTimeout(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更新後狀態的。

相關文章
相關標籤/搜索