事情是這樣的, 這是一個在某天的默默的開發中, 筆者發現了一個驚天地泣鬼神的抓破腦殼都想不破的問題, 然後在這個月黑風高的晚上, 通過對源碼的窺探終於發現原因的悲慘故事
我們先來看一個demo, 關於$nextTick的使用這裏就不再贅述了
<!-- html結構非常的easy, 在vue接管的id爲app的dom區域內渲染了msg -->
<div id='#app'>
{{ msg }}
</div>
const vm = new Vue({
el: '#app',
data: {
msg: 'helloWorld'
}
})
// 1. 首先頁面中一定會渲染出helloWorld
// 2. 第一個$nextTick
vm.$nextTick(() => {
console.log('我是第一個$nextTick的輸出', vm.msg, vm.$el.innerHTML);
})
// 3. 更改msg的值
vm.msg = 'yes i do';
console.log(vm.msg, vm.$el.innerHTML);
// 4. 第二個$nextTick
vm.$nextTick(() => {
console.log('我是第二個$nextTick的輸出',vm.msg, vm.$el.innerHTML);
})
console.log(vm.msg, vm.$el.innerHTML);
我們可以看到輸出結果如下
筆者一開始是真實的一臉懵逼, 我當時唯一可以確定的是$nextTick是異步的, 當然Vue對於界面的更新本來就是異步的, 這個等於是在說廢話, 但是兩個nextTick的結果竟然不一樣?說好的會延遲到下一次dom更新才執行呢? 於是筆者去Vue的官網仔細的翻了翻
來自vue官方:
Vue 在更新 DOM 時是異步執行的。只要偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的所有數據變更。如果同一個 watcher 被多次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免不必要的計算和 DOM 操作是非常重要的。然後,在下一個的事件循環“tick”中,Vue 刷新隊列並執行實際 (已去重的) 工作。
翻譯成人話就是 vue的每次數據更新都會在下一個事件循環(eventloop)中執行
那麼結合我們上方的輸出結果, 筆者短暫認爲$nextTick是在下一次事件循環中dom更新完畢後執行
那麼問題來了: 既然$nextTick會在dom更新後執行, 爲何第一個中打印dom的值依舊沒有發生改變呢?既然沒改變就意味着他沒有在dom更新後執行啊? 這到底啥情況
如果你不看源碼 一定是百思不得其解的, 因爲vue有時候確實設計的非常精妙
筆者來用自己的方法給你寫一寫你能夠看的明白的$nextTick, 跟着註釋看我相信你是不會迷路的
const nextTick = (function() {
let callbacks = []; // 最後所有在nextTick中傳遞過來的函數都會進入這個數組
let timerHandler = () => { // 這個函數用來延遲nexTick傳遞進來的函數的執行
// Promise.resolve這句話往這裏一站, 你就知道這哥們後面的那行then代碼要等待了,
const p = Promise.resolve();
p.then(releaseCallbacks); // 等同步任務執行完畢這哥們會執行
}
function releaseCallbacks() { // 作爲p.then的回調 releaseCallbacks肯定也會在微隊列中等待
for(let i = 0; i < callbacks.length; i++) {
callbacks[i](); // 所有存儲進callbacks的函數挨個執行
}
callbacks.length = 0;
}
// 真正暴露給用戶的回調函數
return function(cb) {
callbacks.push(cb);
timerHandler();
}
}())
調用我們自己的的nextTick方法,其他語句都不變 我們走一遍輸出發現輸出結果如下
確實發現所有交付給nextTick的函數都按照異步執行了, 但是並沒有如我們所想象的那樣, 相反連nextTick真正的作用都發揮不上了, 我們不再可以監聽到msg被更改, 於是我們來看看被筆者進行註釋過後的真正的$nextTick源碼(當然, 前提是上面筆者的這份簡化版源碼你已經看懂了, 不然vue源碼會更加頭大)
export let isUsingMicroTask = false // 這是vue用來判斷是否啓用微任務的鎖, 如果不懂沒關係他不重要
const callbacks = [] // 同樣, 最後所有在nextTick中傳遞過來的函數都會進入這個數組
let pending = false // 異步鎖, 如果同步任務未執行完, 異步鎖肯定是鎖住的
function flushCallbacks () { // 最終執行callbacks的函數
pending = false // 重置異步鎖
// 這裏我們發現將callbacks複製了一份給copies, 最終循環操作的也是copies, 這是因爲不想造成nextTick嵌套調用的衝突
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc // 相當於上面的timerHandler
// 判斷當前環境支不支持原生的Promise構造函數
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 如果支持會走上面的timerHandler的流程
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 判斷是不是IE
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)
}
}
// 真正暴露出去的nextTick方法
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 這個是新加的, 如果沒有傳遞cb參數則返回一個新的promoise出去
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
拋開一些兼容性寫法和一些容錯機制來說, vue的nextTick和我們寫的nextTick沒有什麼差別, 但是爲什麼會產生截然不同的效果呢?
繼續閱讀源碼筆者有發現, vue中還存在一個queueWatcher
方法, 如下
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
if (!config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue);
}
}
}
你不用將他看懂, 但是筆者可以告訴你這哥們的作用就是用來更改nextTick的執行順序的
本身我們執行nextTick他的效果跟一般的異步任務沒什麼太大的區別, 無非就是nextTick會被置於微任務, 而queueWatcher
方法和他帶來的一些騷操作則改變了nextTick的運行軌跡
-
如果在$nextTick前沒有更改vue監控的屬性值的情況發生, 那麼nextTick中的代碼按照正常異步微任務走掉
-
如果在$nextTick前有更改了vue所監控的屬性值的情況, 則queueWatcher會調換nextTick的執行順序, nextTick將會在下一次事件循環vue刷新頁面後執行
有些東西你不看源碼是真的想破頭都想不出來他到底是什麼原因, 這也是我們作爲開發者一直要追逐的事情, 共勉