在掘金刷到有人寫$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引擎線程是互斥的,爲了防止DOM渲染的不一致性,其中一個線程執行時另外一個線程會被掛起。ios
這些線程中,和Vue的nextTick
息息相關的是JS引擎線程和事件觸發線程。git
瀏覽器頁面初次渲染完畢後,JS引擎線程結合事件觸發線程的工做流程以下:github
(1)同步任務在JS引擎線程(主線程)上執行,造成執行棧(Execution Context Stack)。promise
(2)主線程以外,事件觸發線程管理着一個任務隊列(Task Queue)。只要異步任務有了運行結果,就在任務隊列之中放置一個事件。瀏覽器
(3)執行棧中的同步任務執行完畢,系統就會讀取任務隊列,若是有異步任務須要執行,將其加到主線程的執行棧並執行相應的異步任務。
主線程的執行流程以下圖所示:
這裏多是不夠嚴謹的,在本文中事件隊列和任務隊列指向同一個概念。
事件觸發線程管理的任務隊列是如何產生的呢?事實上這些任務就是從JS引擎線程自己產生的,主線程在運行時會產生執行棧,棧中的代碼調用某些異步API時會在任務隊列中添加事件,棧中的代碼執行完畢後,就會讀取任務隊列中的事件,去執行事件對應的回調函數,如此循環往復,造成事件循環機制,以下圖所示:
JS中有兩種任務類型:微任務(microtask)和宏任務(macrotask),在ES6中,microtask稱爲 jobs,macrotask稱爲 task。
宏任務: script (主代碼塊)、setTimeout
、setInterval
、setImmediate
、I/O 、UI rendering
微任務:process.nextTick
(Nodejs) 、promise
、Object.observe
、MutationObserver
這裏要重點說明一下,宏任務並不是全是異步任務,主代碼塊就是屬於宏任務的一種(Promises/A+規範)。
它們之間區別以下:
setTimeout
(下一個宏任務)會更快,由於無需等待UI渲染。自我灌輸一下本身的理解:
根據事件循環機制,從新梳理一下流程:
舉個栗子,如下示例沒法直觀的表述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
事件會加入宏任務隊列,MutationObserver
和Promise
的回調會加入微任務隊列,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'
}]
}
複製代碼
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.
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
還有沒有什麼必然聯繫。
/* @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
})
}
}
複製代碼
/* @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
})
}
}
複製代碼
本文的表述可能存在一些不嚴謹的地方。