衆所周知,爲了與瀏覽器進行交互,Javascript是一門非阻塞單線程腳本語言。vue
用一張圖展現這個過程:ios
在實際狀況中,上述的任務隊列(task queue)中的異步任務分爲兩種:微任務(micro task)和宏任務(macro task)。git
microtask和macotask執行規則:github
下面來個簡單例子:web
console.log(1);promise
setTimeout(function() {瀏覽器
console.log(2);app
}, 0);dom
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按順序執行:4 5
3.microtask清空後執行下一個macrotask:2
再來一個複雜的例子:
// 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.
這樣一個功能和事件循環很是類似,在每一個 task 運行完之後,UI 都會重渲染,那麼很容易想到在 microtask 中就完成數據更新,當前 task 結束就能夠獲得最新的 UI 了。反之若是新建一個 task 來作數據更新,那麼渲染就會進行兩次。
因此在vue 2.4以前使用microtask實現nextTick,直接上源碼
var counter = 1var 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 microTimerFunclet macroTimerFunclet 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。