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

  在閱讀 nextTick 的源碼以前,要先弄明白 JS 執行環境運行機制,介紹 JS 執行環境的事件循環機制的文章不少,大部分都闡述的比較籠統,甚至有些文章說的是錯誤的,如下爲我的理解,若有錯誤,歡迎指正。
css

1、瀏覽器中的進程與線程

  以 chorme 瀏覽器爲例,瀏覽器中的每一個頁面都是一個獨立的進程,在該進程中擁有多個線程,一般有如下幾個常駐線程:
html

一、GUI 渲染線程
二、JavaScript引擎線程
三、定時觸發器線程
四、事件觸發線程
五、異步http請求線程
java

  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 引擎線程讀取。
ios

2、Event Loop

  Javascript 任務分爲同步任務異步任務,同步任務是指調用以後馬上獲得結果的任務;異步任務是指調用以後沒法馬上獲得結果,須要進行額外操做的任務。
  JS 引擎線程順序執行執行棧中的任務,執行棧中只有同步任務,遇到異步任務就交給相應的線程處理。例如在代碼塊中有 setTimeout() 方法的調用,則將其交由定時觸發器線程處理,定時結束以後定時觸發器線程將方法的回調放入自身的任務隊列中,當執行棧中的任務處理完以後會讀取各線程中任務隊列中的事件。
  前面是從同步異步的角度來劃分任務的,從執行順序來講,任務也分爲兩種:macrotask(宏任務)、microtask(微任務)。異步的 macrotask 執行完以後返回的事件會放在各線程的任務隊列中,microtask 執行完以後返回的事件會放在微任務隊列中。
ajax

macrotask包括:script(JS文件)、MessageChannel、setTimeout、setInterval、setImmediate、I/O、ajax、eventListener、UI rendering。
microtask包括:Promise、MutationObserver、已廢棄的Object.observe()、Node中的process.nextTick
數組

  其中須要注意的是GUI 渲染線程去渲染頁面也是以 macrotask 的形式進行的,這個以後詳談。
瀏覽器

  JS 執行環境運行機制——Event Loop(事件循環)的過程如上圖所示:
一、 JS 引擎線程順序執行 執行棧中的任務,以一個 macrotask 爲單位,在單個宏任務沒有處理完以前, JS 引擎線程不會將程序交由 GUI 渲染線程接管。也就是說耗時的任務會阻塞渲染,致使頁面卡頓的狀況發生。典型瀏覽器通常1秒鐘插入60個渲染幀,也就是說16ms進行一次渲染,單個任務超過16ms,若是渲染樹發生改變將得不到及時更新渲染。
  流暢的頁面中通常任務執行狀況以下所示:
  單個任務耗時較多,則會發生丟幀的狀況:
二、 JS 引擎線程在執行 macrotask 時,會將遇到的異步任務交給指定的線程處理。當異步任務爲 macrotask 時,對應線程處理完畢以後 放入線程自身的任務隊列中;若異步任務爲 microtask 時,對應線程處理完畢以後 放入微任務隊列中。macrotask 執行完以後會遍歷微任務隊列中的任務加以執行,清空微任務隊列。
三、當 執行棧中的任務執行完畢後,會讀取各個線程中的任務隊列,將各任務隊列中的事件添加到 執行棧中開始執行。從讀取各任務隊列中的事件放入 執行棧中到清空微任務隊列的過程稱爲一個「tick」。JS引擎線程會循環不斷地讀取任務、處理任務,這個就稱爲 Event Loop(事件循環)機制。

3、nextTick的實現

  Vue的數據更新採用的是異步更新的方式,這樣的好處是數據屬性屢次求值只不用重複調用渲染函數,可以大幅提升性能。其中,異步更新隊列是經過調用 nextTick 方法完成的。
  Vue是數據驅動的框架,最好的狀況是在頁面從新渲染前完成數據的更新。從前面的講述中能夠知道,瀏覽器的運行機制是首先執行 macrotask,而後執行 microtask ,清空微任務隊列後,再從各線程的任務隊列中讀取新的事件以前,GUI 渲染線程有可能接管程序,完成頁面從新渲染。
  nextTick() 在2.5版本以後被單獨提取到一個 js 文件中,而且改變了其實現方式。下面分別介紹兩種具體實現狀況:
框架

一、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 數組中的狀況發生。

二、老版本實現方式

  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 的使用。
如需轉載,煩請註明出處:juejin.im/post/5cd90c…

相關文章
相關標籤/搜索