Vue2.0原始碼閱讀筆記(四):nextTick

白馬笑西風發表於2019-05-13

  在閱讀 nextTick 的原始碼之前,要先弄明白 JS 執行環境執行機制,介紹 JS 執行環境的事件迴圈機制的文章很多,大部分都闡述的比較籠統,甚至有些文章說的是錯誤的,以下為個人理解,如有錯誤,歡迎指正。

一、瀏覽器中的程式與執行緒

  以 chorme 瀏覽器為例,瀏覽器中的每個頁面都是一個獨立的程式,在該程式中擁有多個執行緒,通常有以下幾個常駐執行緒:

1、GUI 渲染執行緒

2、JavaScript引擎執行緒

3、定時觸發器執行緒

4、事件觸發執行緒

5、非同步http請求執行緒

  GUI 渲染執行緒解析 html 生成 DOM 樹,解析 css 生成 CSSOM 樹,然後將兩棵樹合併成渲染樹,最後根據渲染樹畫出介面。當 DOM 的修改導致了樣式非幾何屬性的變化時,渲染執行緒重新繪製新的樣式,稱為“重繪”;當 DOM 的修改導致了樣式幾何屬性的變化,渲染執行緒會重新計算元素的集合屬性,然後將結果繪製出來,稱為“迴流”。

  JS 引擎執行緒負責處理Javascript指令碼程式,且與GUI 渲染執行緒是互斥的,因為 js 是可以操控 DOM 的,如果這兩個執行緒並行會導致錯誤。JS 引擎執行緒與其他可以並行的執行緒配合來實現稱為Event Loop的 javaScript 執行環境執行機制。

  JS 的執行環境是單執行緒的,在程式碼中如果呼叫形如 setTimeout() 這樣的計時功能的 API ,JS 引擎執行緒會將該任務交給定時觸發器執行緒。定時觸發器執行緒在定時結束之後會將任務放入任務佇列中,等待 JS 引擎執行緒讀取。

  JS 與 HTML 之間的互動是通過事件來實現的。在 JS 程式碼中使用偵聽器來預定事件,以便事件發生時執行相應的程式碼,該程式碼稱為事件處理程式或者事件偵聽器。例如點選事件的事件偵聽器是 onclick 。JS 引擎執行緒在執行偵聽 DOM 元素的程式碼時,會將該任務交給事件觸發執行緒處理,當事件被觸發時,事件觸發執行緒會將任務放入任務佇列中,等待 JS 引擎執行緒讀取。

  JS 程式碼中通過 XMLHttpRequest 發起 ajax 請求時,會使用非同步http請求執行緒來管理,在狀態改變時,該執行緒會將對應的回撥放入任務佇列中,等待 JS 引擎執行緒讀取。

二、Event Loop

  Javascript 任務分為同步任務非同步任務,同步任務是指呼叫之後立刻得到結果的任務;非同步任務是指呼叫之後無法立刻得到結果,需要進行額外操作的任務。

  JS 引擎執行緒順序執行執行棧中的任務,執行棧中只有同步任務,遇到非同步任務就交給相應的執行緒處理。例如在程式碼塊中有 setTimeout() 方法的呼叫,則將其交由定時觸發器執行緒處理,定時結束之後定時觸發器執行緒將方法的回撥放入自身的任務佇列中,當執行棧中的任務處理完之後會讀取各執行緒中任務佇列中的事件。

  前面是從同步非同步的角度來劃分任務的,從執行順序來說,任務也分為兩種:macrotask(巨集任務)、microtask(微任務)。非同步的 macrotask 執行完之後返回的事件會放在各執行緒的任務佇列中,microtask 執行完之後返回的事件會放在微任務佇列中。

macrotask包括:script(JS檔案)、MessageChannel、setTimeout、setInterval、setImmediate、I/O、ajax、eventListener、UI rendering。

microtask包括:Promise、MutationObserver、已廢棄的Object.observe()、Node中的process.nextTick

  其中需要注意的是GUI 渲染執行緒去渲染頁面也是以 macrotask 的形式進行的,這個之後詳談。

Vue2.0原始碼閱讀筆記(四):nextTick
  JS 執行環境執行機制——Event Loop(事件迴圈)的過程如上圖所示:

1、JS 引擎執行緒順序執行執行棧中的任務,以一個 macrotask 為單位,在單個巨集任務沒有處理完之前,JS 引擎執行緒不會將程式交由GUI 渲染執行緒接管。也就是說耗時的任務會阻塞渲染,導致頁面卡頓的情況發生。典型瀏覽器一般1秒鐘插入60個渲染幀,也就是說16ms進行一次渲染,單個任務超過16ms,如果渲染樹發生改變將得不到及時更新渲染。

  流暢的頁面中一般任務執行情況如下所示:

Vue2.0原始碼閱讀筆記(四):nextTick
  單個任務耗時較多,則會發生丟幀的情況:

Vue2.0原始碼閱讀筆記(四):nextTick
2、JS 引擎執行緒在執行 macrotask 時,會將遇到的非同步任務交給指定的執行緒處理。當非同步任務為 macrotask 時,對應執行緒處理完畢之後放入執行緒自身的任務佇列中;若非同步任務為 microtask 時,對應執行緒處理完畢之後放入微任務佇列中。macrotask 執行完之後會遍歷微任務佇列中的任務加以執行,清空微任務佇列。

3、當執行棧中的任務執行完畢後,會讀取各個執行緒中的任務佇列,將各任務佇列中的事件新增到執行棧中開始執行。從讀取各任務佇列中的事件放入執行棧中到清空微任務佇列的過程稱為一個“tick”。JS引擎執行緒會迴圈不斷地讀取任務、處理任務,這個就稱為Event Loop(事件迴圈)機制。

三、nextTick的實現

  Vue的資料更新採用的是非同步更新的方式,這樣的好處是資料屬性多次求值只不用重複呼叫渲染函式,能夠大幅提高效能。其中,非同步更新佇列是通過呼叫 nextTick 方法完成的。

  Vue是資料驅動的框架,最好的情況是在頁面重新渲染前完成資料的更新。從前面的講述中可以知道,瀏覽器的執行機制是首先執行 macrotask,然後執行 microtask ,清空微任務佇列後,再從各執行緒的任務佇列中讀取新的事件之前,GUI 渲染執行緒有可能接管程式,完成頁面重新渲染。

  nextTick() 在2.5版本之後被單獨提取到一個 js 檔案中,並且改變了其實現方式。下面分別介紹兩種具體實現情況:

1、Vue2.5+ 版本實現方式

  Vue2.5.22 版本的 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
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

  首先說明其中三個變數,callbacks 是儲存非同步更新回撥的任務佇列、pending 標識任務佇列是否正在重新整理、useMacroTask 變數表明是否強制使用 macrotask 方式執行回撥。

  nextTick() 註冊一個執行傳入回撥的函式放入到 callbacks 陣列中,如果沒有傳入回撥則返回 Promise 物件。如果佇列沒有開始重新整理,則將等待重新整理標識設為 true,開始重新整理任務。如果沒有強制指明需要使用 macrotask 的方式重新整理,則預設呼叫 microTimerFunc 方法來執行。

  microTimerFunc 方法的實現如下程式碼所示:

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => { setImmediate(flushCallbacks) }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => { port.postMessage(1) }
} else {
  macroTimerFunc = () => { setTimeout(flushCallbacks, 0) }
}

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
} else {
  microTimerFunc = macroTimerFunc
}

  microTimerFunc 方法實質就是將 flushCallbacks 方法註冊成非同步任務加以執行。

  優先使用 Promise 的方式將 flushCallbacks() 的執行註冊成 microtask;其中需要注意的是在有的ios環境下,即使將任務推到微任務佇列中,佇列也不會馬上重新整理,直到瀏覽器需要做一些其它的工作,因此在此處新增一個空的計時器來使微任務佇列重新整理。

  如果環境不相容 Promise,則將 flushCallbacks() 的執行註冊成 macrotask。優先使用 setImmediate 註冊任務,setImmediate() 效能好、優先順序高,但是相容性很差,目前只有 IE 瀏覽器支援。其次使用 MessageChannel 實現,如果都不支援,則呼叫 setTimeout() 實現。

  flushCallbacks() 的實現方式如下所示:

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

  首先將是否重新整理的標識設為 false ,然後複製 callbacks 陣列到 copies ,再清空 callbacks 陣列,遍歷 copies 執行每一個回撥。這裡將 callbacks 清空、遍歷複製陣列 copies 的原因是為了防止在遍歷執行回撥的過程中,不斷有新的回撥新增到 callbacks 陣列中的情況發生。

2、老版本實現方式

  Vue2.4.4 版本的 nextTick() 實現與2.5+ 版本的差異主要是下面這段程式碼:

  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      if (isIOS) setTimeout(noop)
    }
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, { characterData: true })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    timerFunc = () => {setTimeout(nextTickHandler, 0)}
  }

  老版本的 nextTick() 與2.5+ 版本的最主要區別是將任務註冊成非同步佇列的方式不同。優先使用 Promise 將任務註冊成 microtask,其次使用 MutationObserver 將任務註冊成 microtask。如果環境不允許將任務註冊成 microtask,則直接使用 setTimeout() 將任務註冊成 macrotask。

  可以看出老版本的 nextTick() 對效能的追求特別高,基本上都是採用 microtask 來實現非同步更新的,macrotask 沒有區分層級,直接使用 setTimeout() 來最後兜底。

  MutationObserver 的優先順序特別高,在某些場景下它甚至要比事件冒泡還要快,會導致很多問題。如果全部使用 macrotask 則對一些有重繪和動畫的場景也會有效能影響。所以 Vue2.5+ 版本刪除了對 MutationObserver 的使用,增強了 macrotask 的使用。

如需轉載,煩請註明出處:https://www.cnblogs.com/lidengfeng/p/10856352.html

相關文章