前言
熟悉 vue 的前端,想必對 vue 里的 nextTick 也很熟悉了,用的時候就知道他是延遲回調,有時候用起來甚至和setTimeout 看起來是同樣的效果。但他和setTimeout到底有什么區別?他是如何實現的?
本文就nextTick的實現引入,來探討下js中的異步與同步,微任務與宏任務。
nextTick
用法
先搬運下文檔 Vue-nextTick
在下次 DOM 更新循環結束之后執行延遲回調。在修改數據之后立即使用這個方法,獲取更新后的 DOM
// 修改數據
vm.msg = 'Hello'
// DOM 還沒有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作為一個 Promise 使用 (2.1.0 起新增,詳見接下來的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
源碼實現
在了解原理之前先看下 nextTick
源碼實現
// 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 */
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.
// Technically 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)
}
}
可以看到上面有幾個條件判斷 如果支持 Promise 就用 Promise
如果不支持就用 MutationObserver MDN-MutationObserver
MutationObserver 它會在指定的DOM發生變化時被調用
如果不支持 MutationObserver 的話就用 setImmediate MDN-setImmediate
但是這個特性只有最新版IE和node支持,然后是最后一個條件 如果這些都不支持的話就用setTimeout。
看完這一段其實也很懵,為什么要這樣設計呢?為什么要這樣一個順序來判斷呢?說到這里就不得不討論JavaScript 運行機制(Event Loop)&微任務宏任務了。
JavaScript 運行機制(Event Loop)
單線程
JS是單線程,同一個時間只能做一件事。至於JS為什么是單線程?
JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為准?所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。
同步和異步
js里的任務分為兩種:同步任務(synchronous)和異步任務(asynchronous)。同步阻塞異步非阻塞。
同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務,例如alert,會阻塞后續任務的執行,只有在點擊確定之后,才會執行下一個任務。
異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。
單線程就意味着,所有任務需要排隊,前一個任務結束,才會執行后一個任務。所以會有任務隊列的概念。正因為是單線程,所以所有任務都是主線程執行的,異步請求這些也不會開辟新的線程,而是放到任務隊列,當這些異步操作被觸發時才進入主線程執行。
宏任務和微任務
JS任務又分為宏任務和微任務。
宏任務(macrotask):setTimeout、setInterval、setImmediate、I/O、UI rendering
微任務(microtask):promise.then、process.nextTick、MutationObserver、queneMicrotask(開啟一個微任務)
宏任務按順序執行,且瀏覽器在每個宏任務之間渲染頁面
瀏覽器為了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束后,在下一個 task 執行開始前,對頁面進行重新渲染 (task->渲染->task->...)
微任務通常來說就是需要在當前 task 執行結束后立即執行的任務,比如對一系列動作做出反饋,或或者是需要異步的執行任務而又不需要分配一個新的 task,這樣便可以減小一點性能的開銷。只要執行棧中沒有其他的js代碼正在執行且每個宏任務執行完,微任務隊列會立即執行。如果在微任務執行期間微任務隊列加入了新的微任務,會將新的微任務加入隊列尾部,之后也會被執行。
何時使用微任務
微任務的執行時機,晚於當前本輪事件循環的 Call Stack(調用棧)中的代碼(宏任務),遭遇事件處理函數和定時器的回調函數
使用微任務的原因
減少操作中用戶可感知到的延遲
確保任務順序的一致性,即便當結果或數據是同步可用的
批量操作的優化
了解了宏任務和微任務的執行順序,就可以了解到為何nextTick 要優先使用Promise
和MutationObserver
因為他倆屬於微任務,會在執行棧空閑的時候立即執行,它的響應速度相比setTimeout會更快,因為無需等渲染。
而setImmediate和setTimeout屬於宏任務,執行開始之前要等渲染,即task->渲染->task。
參考:
阮一峰-JavaScript 運行機制詳解:再談Event Loop
譯文:JS事件循環機制(event loop)之宏任務、微任務
Generator 函數的含義與用法
瀏覽器進程、JS事件循環機制、宏任務和微任務