前言
眾所周知,為了與瀏覽器進行交互,Javascript是一門非阻塞單線程腳本語言。
為何單線程? 因為如果在DOM操作中,有兩個線程一個添加節(jié)點,一個刪除節(jié)點,瀏覽器并不知道以哪個為準,所以只能選擇一個主線程來執(zhí)行代碼,以防止沖突。雖然如今添加了webworker等新技術(shù),但其依然只是主線程的子線程,并不能執(zhí)行諸如I/O類的操作。長期來看,JS將一直是單線程。
-
為何非阻塞?因為單線程意味著任務(wù)需要排隊,任務(wù)按順序執(zhí)行,如果一個任務(wù)很耗時,下一個任務(wù)不得不等待。所以為了避免這種阻塞,我們需要一種非阻塞機制。這種非阻塞機制是一種異步機制,即需要等待的任務(wù)不會阻塞主執(zhí)行棧中同步任務(wù)的執(zhí)行。這種機制是如下運行的:
- 所有同步任務(wù)都在主線程上執(zhí)行,形成一個
執(zhí)行棧(execution context stack) - 等待任務(wù)的回調(diào)結(jié)果進入一種
任務(wù)隊列(task queue)。 - 當(dāng)主執(zhí)行棧中的同步任務(wù)執(zhí)行完畢后才會讀取
任務(wù)隊列,任務(wù)隊列中的異步任務(wù)(即之前等待任務(wù)的回調(diào)結(jié)果)會塞入主執(zhí)行棧, - 異步任務(wù)執(zhí)行完畢后會再次進入下一個循環(huán)。此即為今天文章的主角
事件循環(huán)(Event Loop)
用一張圖展示這個過程:
Markdown - 所有同步任務(wù)都在主線程上執(zhí)行,形成一個
正文
1.macro task與micro task
在實際情況中,上述的任務(wù)隊列(task queue)中的異步任務(wù)分為兩種:微任務(wù)(micro task)和宏任務(wù)(macro task)。
- micro task事件:
Promises(瀏覽器實現(xiàn)的原生Promise)、MutationObserver、process.nextTick
<br /> - macro task事件:
setTimeout、setInterval、setImmediate、I/O、UI rendering
這里注意:script(整體代碼)即一開始在主執(zhí)行棧中的同步代碼本質(zhì)上也屬于macrotask,屬于第一個執(zhí)行的task
microtask和macotask執(zhí)行規(guī)則:
- macrotask按順序執(zhí)行,瀏覽器的ui繪制會插在每個macrotask之間
- microtask按順序執(zhí)行,會在如下情況下執(zhí)行:
- 每個callback之后,只要沒有其他的JS在主執(zhí)行棧中
- 每個macrotask結(jié)束時
下面來個簡單例子:
console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
new Promise(function(resolve,reject){
console.log(3)
resolve()
}).then(function() {
console.log(4);
}).then(function() {
console.log(5);
});
console.log(6);
一步一步分析如下:
- 1.同步代碼作為第一個macrotask,按順序輸出:1 3 6
- 2.microtask按順序執(zhí)行:4 5
- 3.microtask清空后執(zhí)行下一個macrotask:2
再來一個復(fù)雜的例子:
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
// Here's a click listener…
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
假設(shè)我們創(chuàng)建一個有里外兩部分的正方形盒子,里外都綁定了點擊事件,此時點擊內(nèi)部,代碼會如何執(zhí)行?一步一步分析如下:
- 1.觸發(fā)內(nèi)部click事件,同步輸出:click
- 2.將setTimeout回調(diào)結(jié)果放入macrotask隊列
- 3.將promise回調(diào)結(jié)果放入microtask
- 4.將Mutation observers放入microtask隊列,主執(zhí)行棧中onclick事件結(jié)束,主執(zhí)行棧清空
- 5.依序執(zhí)行microtask隊列中任務(wù),輸出:promise mutate
- 6.注意此時事件冒泡,外部元素再次觸發(fā)onclick回調(diào),所以按照前5步再次輸出:click promise mutate(我們可以注意到事件冒泡甚至?xí)趍icrotask中的任務(wù)執(zhí)行之后,microtask優(yōu)先級非常高)
- 7.macrotask中第一個任務(wù)執(zhí)行完畢,依次執(zhí)行macrotask中剩下的任務(wù)輸出:timeout timeout
2.vue.nextTick實現(xiàn)
在 Vue.js 里是數(shù)據(jù)驅(qū)動視圖變化,由于 JS 執(zhí)行是單線程的,在一個 tick 的過程中,它可能會多次修改數(shù)據(jù),但 Vue.js 并不會傻到每修改一次數(shù)據(jù)就去驅(qū)動一次視圖變化,它會把這些數(shù)據(jù)的修改全部 push 到一個隊列里,然后內(nèi)部調(diào)用 一次 nextTick 去更新視圖,所以數(shù)據(jù)到 DOM 視圖的變化是需要在下一個 tick 才能完成。這便是我們?yōu)槭裁葱枰?code>vue.nextTick.
這樣一個功能和事件循環(huán)非常相似,在每個 task 運行完以后,UI 都會重渲染,那么很容易想到在 microtask 中就完成數(shù)據(jù)更新,當(dāng)前 task 結(jié)束就可以得到最新的 UI 了。反之如果新建一個 task 來做數(shù)據(jù)更新,那么渲染就會進行兩次。
所以在vue 2.4之前使用microtask實現(xiàn)nextTick,直接上源碼
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)
}
可以看到使用了MutationObserver
然而到了vue 2.4之后卻混合?使用microtask macrotask來實現(xiàn),源碼如下
/* @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]()
}
}
// Here we have async deferring wrappers using both micro and macro tasks.
// In < 2.4 we used micro tasks everywhere, but there are some scenarios where
// micro tasks have too high a priority and fires 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 micro task by default, but expose a way to force macro task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
// 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)
}
}
// 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
}
/**
* Wrap a function so that if any code inside triggers state change,
* the changes are queued using a Task instead of a MicroTask.
*/
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
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
})
}
}
可以看到使用setImmediate、MessageChannel等mascrotask事件來實現(xiàn)nextTick。
為什么會如此修改,其實看之前的事件冒泡例子就可以知道,由于microtask優(yōu)先級太高,甚至?xí)让芭菘?,所以會造成一些詭異的bug。如 issue #4521、#6690、#6556;但是如果全部都改成 macro task,對一些有重繪和動畫的場景也會有性能影響,如 issue #6813。所以最終 nextTick 采取的策略是默認走 micro task,對于一些 DOM 交互事件,如 v-on 綁定的事件回調(diào)函數(shù)的處理,會強制走 macro task。
