你真的理解$nextTick麼

在掘金刷到有人寫$nextTick,這裏我把我之前的這篇分析文章拿出來給你們看看,但願對你們有所啓迪,這裏是我寫的原文連接地址Vue源碼分析 - nextTick。可能文中有些表述不是很嚴謹,你們見諒。javascript

順便推薦你們看一篇很是好的文章Tasks, microtasks, queues and schedules,看完絕對有所收穫。css

這裏的描述不是很詳細,後續須要補充Node.js的EventLoop和瀏覽器的差別。html

爲何是nextTick

這裏猜想一下爲何Vue有一個API叫nextTick前端

瀏覽器

瀏覽器(多進程)包含了Browser進程(瀏覽器的主進程)、第三方插件進程GPU進程(瀏覽器渲染進程),其中GPU進程(多線程)和Web前端密切相關,包含如下線程:java

  • GUI渲染線程
  • JS引擎線程
  • 事件觸發線程(和EventLoop密切相關)
  • 定時觸發器線程
  • 異步HTTP請求線程

GUI渲染線程JS引擎線程是互斥的,爲了防止DOM渲染的不一致性,其中一個線程執行時另外一個線程會被掛起。ios

這些線程中,和Vue的nextTick息息相關的是JS引擎線程事件觸發線程git

JS引擎線程和事件觸發線程

瀏覽器頁面初次渲染完畢後,JS引擎線程結合事件觸發線程的工做流程以下:github

(1)同步任務在JS引擎線程(主線程)上執行,造成執行棧(Execution Context Stack)。promise

(2)主線程以外,事件觸發線程管理着一個任務隊列(Task Queue)。只要異步任務有了運行結果,就在任務隊列之中放置一個事件。瀏覽器

(3)執行棧中的同步任務執行完畢,系統就會讀取任務隊列,若是有異步任務須要執行,將其加到主線程的執行棧並執行相應的異步任務。

主線程的執行流程以下圖所示:

這裏多是不夠嚴謹的,在本文中事件隊列任務隊列指向同一個概念。

事件循環機制(Event Loop)

事件觸發線程管理的任務隊列是如何產生的呢?事實上這些任務就是從JS引擎線程自己產生的,主線程在運行時會產生執行棧,棧中的代碼調用某些異步API時會在任務隊列中添加事件,棧中的代碼執行完畢後,就會讀取任務隊列中的事件,去執行事件對應的回調函數,如此循環往復,造成事件循環機制,以下圖所示:

任務類型

JS中有兩種任務類型:微任務(microtask)和宏任務(macrotask),在ES6中,microtask稱爲 jobs,macrotask稱爲 task。

宏任務: script (主代碼塊)、setTimeoutsetIntervalsetImmediate 、I/O 、UI rendering

微任務process.nextTick(Nodejs) 、promiseObject.observeMutationObserver

這裏要重點說明一下,宏任務並不是全是異步任務,主代碼塊就是屬於宏任務的一種(Promises/A+規範)。

它們之間區別以下:

  • 宏任務是每次執行棧執行的代碼(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)
  • 瀏覽器爲了可以使得JS引擎線程GUI渲染線程有序切換,會在當前宏任務結束以後,下一個宏任務執行開始以前,對頁面進行從新渲染(宏任務 > 渲染 > 宏任務 > ...)
  • 微任務是在當前宏任務執行結束以後當即執行的任務(在當前 宏任務執行以後,UI渲染以前執行的任務)。微任務的響應速度相比setTimeout(下一個宏任務)會更快,由於無需等待UI渲染。
  • 當前宏任務執行後,會將在它執行期間產生的全部微任務都執行一遍。

自我灌輸一下本身的理解:

  • 宏任務中的事件是由事件觸發線程來維護的
  • 微任務中的全部任務是由JS引擎線程維護的(這只是自我猜想,由於宏任務執行完畢後會當即執行微任務,爲了提高性能,這種無縫鏈接的操做放在事件觸發線程來維護明顯是不合理的)。

根據事件循環機制,從新梳理一下流程:

  • 執行一個宏任務(首次執行的主代碼塊或者任務隊列中的回調函數)
  • 執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中
  • 宏任務執行完畢後,當即執行當前微任務隊列中的全部任務(依次執行)
  • JS引擎線程掛起,GUI線程執行渲染
  • GUI線程渲染完畢後掛起,JS引擎線程執行任務隊列中的下一個宏任務

舉個栗子,如下示例沒法直觀的表述UI渲染線程的接管過程,只是表述了JS引擎線程的執行流程:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .outer { height: 200px; background-color: red; padding: 10px; } .inner { height: 100px; background-color: blue; margin-top: 50px; } </style> </head> <body> <div class="outer"> <div class="inner"></div> </div> </body> <script> let inner = document.querySelector('.inner') let outer = document.querySelector('.outer') // 監聽outer元素的attribute變化 new MutationObserver(function() { console.log('mutate') }).observe(outer, { attributes: true }) // click監聽事件 function onClick() { console.log('click') setTimeout(function() { console.log('timeout') }, 0) Promise.resolve().then(function() { console.log('promise') }) outer.setAttribute('data-random', Math.random()) } inner.addEventListener('click', onClick) </script> </html> 複製代碼

點擊inner元素打印的順序是:建議放入瀏覽器驗證。

觸發的click事件會加入宏任務隊列,MutationObserverPromise的回調會加入微任務隊列,setTimeout加入到宏任務隊列,對應的任務用對象直觀的表述一下(自我認知的一種表述,只有參考價值):

{
 // tasks是宏任務隊列
  tasks: [{
	script: '主代碼塊'
  }, {
    script: 'click回調函數',
   // microtasks是微任務隊列
    microtasks: [{ 
      script: 'Promise'
    }, {
      script: 'MutationObserver'
    }]
  }, {
    script: 'setTimeout'
  }]
}
複製代碼

稍微增長一下代碼的複雜度,在原有的基礎上給outer元素新增一個click監聽事件:

outer.addEventListener('click', onClick)
複製代碼

點擊inner元素打印的順序是:建議放入瀏覽器驗證。

因爲冒泡,click函數再一次執行了,對應的任務用對象直觀的表述一下(自我認知的一種表述,只有參考價值):

{
  tasks: [{
	script: '主代碼塊'
  }, {
    script: 'innter的click回調函數',
    microtasks: [{
      script: 'Promise'
    }, {
      script: 'MutationObserver'
    }]
  }, {
    script: 'outer的click回調函數',
    microtasks: [{
      script: 'Promise'
    }, {
      script: 'MutationObserver'
    }]
  }, {
    script: 'setTimeout'
  }, {
    script: 'setTimeout'
  }]
}
複製代碼

Node.js中的process.nextTick

Node.js中有一個nextTick函數和Vue中的nextTick命名一致,很容易讓人聯想到一塊兒(Node.js的Event Loop和瀏覽器的Event Loop有差別)。重點講解一下Node.js中的nextTick的執行機制,簡單的舉個栗子:

setTimeout(function() {
  console.log('timeout')
})

process.nextTick(function(){
  console.log('nextTick 1')
})

new Promise(function(resolve){
  console.log('Promise 1')
  resolve();
  console.log('Promise 2')
}).then(function(){
  console.log('Promise Resolve')
})

process.nextTick(function(){
  console.log('nextTick 2')
})
複製代碼

在Node環境(10.3.0版本)中打印的順序: Promise 1 > Promise 2 > nextTick 1 > nextTick 2 > Promise Resolve > timeout

在Node.js的v10.x版本中對於process.nextTick的說明以下:

The process.nextTick() method adds the callback to the "next tick queue". Once the current turn of the event loop turn runs to completion, all callbacks currently in the next tick queue will be called. This is not a simple alias to setTimeout(fn, 0). It is much more efficient. It runs before any additional I/O events (including timers) fire in subsequent ticks of the event loop.

Vue的API命名nextTick

Vue官方對nextTick這個API的描述:

在下次 DOM 更新循環結束以後執行延遲迴調。在修改數據以後當即使用這個方法,獲取更新後的 DOM。

// 修改數據
vm.msg = 'Hello'
// DOM 尚未更新
Vue.nextTick(function () {
  // DOM 更新了
})

// 做爲一個 Promise 使用 (2.1.0 起新增,詳見接下來的提示)
Vue.nextTick()
 .then(function () {
  // DOM 更新了
})
複製代碼

2.1.0 起新增:若是沒有提供回調且在支持 Promise 的環境中,則返回一個 Promise。請注意 Vue 不自帶 Promise 的 polyfill,因此若是你的目標瀏覽器不原生支持 Promise (IE:大家都看我幹嗎),你得本身提供 polyfill。 0

可能你尚未注意到,Vue 異步執行 DOM 更新。只要觀察到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據改變。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做上很是重要。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。Vue 在內部嘗試對異步隊列使用原生的 Promise.then 和 MessageChannel,若是執行環境不支持,會採用 setTimeout(fn, 0) 代替。

例如,當你設置 vm.someData = 'new value' ,該組件不會當即從新渲染。當刷新隊列時,組件會在事件循環隊列清空時的下一個「tick」更新。多數狀況咱們不須要關心這個過程,可是若是你想在 DOM 狀態更新後作點什麼,這就可能會有些棘手。雖然 Vue.js 一般鼓勵開發人員沿着「數據驅動」的方式思考,避免直接接觸 DOM,可是有時咱們確實要這麼作。爲了在數據變化以後等待 Vue 完成更新 DOM ,能夠在數據變化以後當即使用 Vue.nextTick(callback) 。這樣回調函數在 DOM 更新完成後就會調用。

Vue對於這個API的感情是曲折的,在2.4版本、2.5版本和2.6版本中對於nextTick進行反覆變更,緣由是瀏覽器對於微任務的不兼容性影響、微任務宏任務各自優缺點的權衡。

看以上流程圖,若是Vue使用setTimeout宏任務函數,那麼勢必要等待UI渲染完成後的下一個宏任務執行,而若是Vue使用微任務函數,無需等待UI渲染完成才進行nextTick的回調函數操做,能夠想象在JS引擎線程GUI渲染線程之間來回切換,以及等待GUI渲染線程的過程當中,瀏覽器勢必要消耗性能,這是一個嚴謹的框架徹底須要考慮的事情。

固然這裏所說的只是nextTick執行用戶回調以後的性能狀況考慮,這中間固然不能忽略flushBatcherQueue更新Dom的操做,使用異步函數的另一個做用固然是要確保同步代碼執行完畢Dom更新性能優化(例如同步操做對響應式數據使用for循環更新一千次,那麼這裏只有一次DOM更新而不是一千次)。

到了這裏,對於Vue中nextTick函數的命名應該是瞭然於心了,固然這個命名不知道和Node.js的process.nextTick還有沒有什麼必然聯繫。

Vue中NextTick源碼(這裏加了一些簡單的註釋說明)

2.5版本

/* @flow */
/* globals MessageChannel */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'

const callbacks = []
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]()
  }
}

// 在2.4中使用了microtasks ,可是仍是存在問題,
// 在2.5版本中組合使用macrotasks和microtasks,組合使用的方式是對外暴露withMacroTask函數
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).

// 2.5版本在nextTick中對於調用microtask(微任務)仍是macrotask(宏任務)聲明瞭兩個不一樣的變量
let microTimerFunc
let macroTimerFunc

// 默認使用microtask(微任務)
let useMacroTask = false


// 這裏主要定義macrotask(宏任務)函數
// macrotask(宏任務)的執行優先級
// setImmediate -> MessageChannel -> setTimeout
// setImmediate是最理想的選擇
// 最Low的情況是降級執行setTimeout

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}


// 這裏主要定義microtask(微任務)函數
// microtask(微任務)的執行優先級
// Promise -> macroTimerFunc
// 若是原生不支持Promise,那麼執行macrotask(宏任務)函數

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}


// 對外暴露withMacroTask 函數
// 觸發變化執行nextTick時強制執行macrotask(宏任務)函數

/** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    try {
      return fn.apply(null, arguments)
    } finally {
      useMacroTask = false    
    }
  })
}

// 這裏須要注意pending
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()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
複製代碼

2.6版本

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
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]()
  }
}

// 在2.5版本中組合使用microtasks 和macrotasks,可是重繪的時候仍是存在一些小問題,並且使用macrotasks在任務隊列中會有幾個特別奇怪的行爲沒辦法避免,So又回到了以前的狀態,在任何地方優先使用microtasks 。
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */


// task的執行優先級
// Promise -> MutationObserver -> setImmediate -> setTimeout

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

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
    })
  }
}
複製代碼

總結

本文的表述可能存在一些不嚴謹的地方。

參考文獻

相關文章
相關標籤/搜索