Vue中nextTick原理解析(附源碼解讀)

來點雞湯

男人一事無成時的溫柔是最廉價的segmentfault

前言

在理解nextTick以前,咱們先要了解js的運行機制瀏覽器

js運行機制

js執行是單線程的,基於事件循環,事件循環大體分爲如下幾個步驟:bash

(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。異步

(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件,能夠看出,這個任務隊列主要存放異步任務函數

(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。ui

(4)主線程不斷重複上面的第三步。this

主線程的執行過程就是一個 tick,而全部的異步結果都是經過 「任務隊列」 來調度。 消息隊列中存放的是一個個的任務(task)。 規範中規定 task 分爲兩大類,分別是 macro task 和 micro task,而且每一個 macro task 執行結束後,都要檢查一遍micro task,並執行清理完成再進入下一輪事件循環。

傳送門:segmentfault.com/a/119000001…spa

nextTick

它的源碼很簡單,
源碼:src/core/util/next-tick.js線程

export let isUsingMicroTask = false
/* 存放異步執行的回調 */
const callbacks = []
/* 標記位,若是已經有timerFunc被推送到任務隊列中去則不須要重複推送 */
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]()
  }
}

/* 一個函數指針,指向函數將被推送到任務隊列中,等到主線程任務執行完時,任務隊列中的timeFunc將被調用 */
let timerFunc
/* 兼容Promise的瀏覽器 */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
  /* 執行微任務標誌位 */
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  /* 不支持Promise則使用MutationObserver */
  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)) {
  /* 不支持Promise和MutationObserver則使用setImmediate */
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  /* 不然都使用setTimeout */
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
/* 延遲一個任務使其異步執行,在下一個tick時執行,這個函數的做用是在task或者microtask中推入一個timerFunc,在當前調用棧執行完之後以此執行直到執行到timerFunc,目的是延遲到當前調用棧執行完之後執行 */
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)
    }
  })
  /* 已經有timerFunc被推送到任務隊列中去則不須要重複推送 */
  if (!pending) {
    pending = true
    timerFunc()
  }
  /* nextTick沒有傳回調函數時,返回一個Promise,咱們能夠nextTick().then(() => {}) */
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

複製代碼

解析:能夠看出,nextTick在內部對環境作了兼容,首先會檢測是否支持微任務:Promise,支持的話則把咱們傳入的回調押入隊列中在下一個tick中等待執行(flushCallbacks依次執行回調),若不支持則使用MutationObserver,如以上二者都不支持則使用宏任務:setImmediate和setTimeout。
最後若是咱們沒有給nextTick傳入回調,則能夠nextTick().then(() => {})跳到then邏輯中。指針

總結

結合上一節的 setter 分析,咱們瞭解到數據的變化到 DOM 的從新渲染是一個異步過程,發生在下一個 tick。這就是咱們平時在開發的過程當中,好比從服務端接口去獲取數據的時候,數據作了修改,若是咱們的某些方法去依賴了數據修改後的 DOM 變化,咱們就必須在 nextTick 後執行。好比下面的僞代碼:

getData(res).then(()=>{
  this.xxx = res.data
  this.$nextTick(() => {
    // 這裏咱們能夠獲取變化後的 DOM
  })
})
複製代碼
相關文章
相關標籤/搜索