衆所周知,爲了與瀏覽器進行交互,Javascript是一門非阻塞單線程腳本語言。
複製代碼
爲什麼單線程? 由於若是在DOM操做中,有兩個線程一個添加節點,一個刪除節點,瀏覽器並不知道以哪一個爲準,因此只能選擇一個主線程來執行代碼,以防止衝突。雖然現在添加了webworker等新技術,但其依然只是主線程的子線程,並不能執行諸如I/O類的操做。長期來看,JS將一直是單線程。javascript
爲什麼非阻塞?由於單線程意味着任務須要排隊,任務按順序執行,若是一個任務很耗時,下一個任務不得不等待。因此爲了不這種阻塞,咱們須要一種非阻塞機制。這種非阻塞機制是一種異步機制,即須要等待的任務不會阻塞主執行棧中同步任務的執行。這種機制是以下運行的:vue
執行棧(execution context stack)
任務隊列(task queue)
。任務隊列
,任務隊列中的異步任務(即以前等待任務的回調結果)會塞入主執行棧,事件循環(Event Loop)
用一張圖展現這個過程: java
在實際狀況中,上述的任務隊列(task queue)
中的異步任務分爲兩種:微任務(micro task)
和宏任務(macro task)
。ios
Promises(瀏覽器實現的原生Promise)
、MutationObserver
、process.nextTick
setTimeout
、setInterval
、setImmediate
、I/O
、UI rendering
這裏注意:script(總體代碼)
即一開始在主執行棧中的同步代碼本質上也屬於macrotask,屬於第一個執行的taskmicrotask和macotask執行規則:git
下面來個簡單例子:github
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);
複製代碼
一步一步分析以下:web
再來一個複雜的例子:promise
// 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);
複製代碼
假設咱們建立一個有裏外兩部分的正方形盒子,裏外都綁定了點擊事件,此時點擊內部,代碼會如何執行?一步一步分析以下:瀏覽器
在 Vue.js 裏是數據驅動視圖變化,因爲 JS 執行是單線程的,在一個 tick 的過程當中,它可能會屢次修改數據,但 Vue.js 並不會傻到每修改一次數據就去驅動一次視圖變化,它會把這些數據的修改所有 push 到一個隊列裏,而後內部調用 一次 nextTick 去更新視圖,因此數據到 DOM 視圖的變化是須要在下一個 tick 才能完成。這即是咱們爲何須要vue.nextTick
.app
這樣一個功能和事件循環很是類似,在每一個 task 運行完之後,UI 都會重渲染,那麼很容易想到在 microtask 中就完成數據更新,當前 task 結束就能夠獲得最新的 UI 了。反之若是新建一個 task 來作數據更新,那麼渲染就會進行兩次。
因此在vue 2.4以前使用microtask實現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來實現,源碼以下
/* @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事件來實現nextTick。
爲何會如此修改,其實看以前的事件冒泡例子就能夠知道,因爲microtask優先級過高,甚至會比冒泡快,因此會形成一些詭異的bug。如 issue #4521、#6690、#6556;可是若是所有都改爲 macro task,對一些有重繪和動畫的場景也會有性能影響,如 issue #6813。因此最終 nextTick 採起的策略是默認走 micro task,對於一些 DOM 交互事件,如 v-on 綁定的事件回調函數的處理,會強制走 macro task。