Vuejs中nextTick()非同步更新佇列原始碼解析
vue官網關於此解釋說明如下:
vue2.0裡面的深入響應式原理的非同步更新佇列
官網說明如下:
只要觀察到資料變化,Vue 將開啟一個佇列,並緩衝在同一事件迴圈中發生的所有資料改變。如果同一個 watcher 被多次觸發,只會一次推入到佇列中。這種在緩衝時去除重複資料對於避免不必要的計算和 DOM 操作上非常重要。然後,在下一個的事件迴圈“tick”中,Vue 重新整理佇列並執行實際(已去重的)工作。Vue 在內部嘗試對非同步佇列使用原生的 Promise.then 和 MutationObserver,如果執行環境不支援,會採用 setTimeout(fn, 0) 代替。
例如,當你設定 vm.someData = ‘new value’ ,該元件不會立即重新渲染。當重新整理佇列時,元件會在事件迴圈佇列清空時的下一個“tick”更新。多數情況我們不需要關心這個過程,但是如果你想在 DOM 狀態更新後做點什麼,這就可能會有些棘手。雖然 Vue.js 通常鼓勵開發人員沿著“資料驅動”的方式思考,避免直接接觸 DOM,但是有時我們確實要這麼做。為了在資料變化之後等待 Vue 完成更新 DOM ,可以在資料變化之後立即使用 Vue.nextTick(callback) 。這樣回撥函式在 DOM 更新完成後就會呼叫。例如
原始碼解析
方法原型以及解析註釋如下:
var nextTick = (function () {
var callbacks = []; // 儲存需要觸發的回撥函式
var pending = false; // 是否正在等待的標識(false:允許觸發在下次事件迴圈觸發callbacks中的回撥, true: 已經觸發過,需要等到下次事件迴圈)
var timerFunc; // 設定在下次事件迴圈觸發callbacks的 觸發函式
//處理callbacks的函式
function nextTickHandler () {
pending = false;// 可以觸發timeFunc
var copies = callbacks.slice(0);//複製callback
callbacks.length = 0;//清空callback
for (var i = 0; i < copies.length; i++) {
copies[i]();//觸發callback回撥函式
}
}
//如果支援Promise,使用Promise實現
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
var logError = function (err) { console.error(err); };
timerFunc = function () {
p.then(nextTickHandler).catch(logError);
// ios的webview下,需要強制重新整理佇列,執行上面的回撥函式
if (isIOS) { setTimeout(noop); }
};
//如果Promise不支援,但是支援MutationObserver(h5新特性,非同步,當dom變動是觸發,注意是所有的dom都改變結束後觸發)
} else if (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 IE11, iOS7, Android 4.4
var counter = 1;
var observer = new MutationObserver(nextTickHandler);
//建立一個textnode dom節點,並讓MutationObserver 監視這個節點;而 timeFunc正是改變這個dom節點的觸發函式
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else {// 上面兩種不支援的話,就使用setTimeout
timerFunc = function () {
setTimeout(nextTickHandler, 0);
};
}
//nextTick接受的函式, 引數1:回撥函式 引數2:回撥函式的執行上下文
return function queueNextTick (cb, ctx) {
var _resolve;//用於接受觸發 promise.then中回撥的函式
//向回撥資料中pushcallback
callbacks.push(function () {
//如果有回撥函式,執行回撥函式
if (cb) { cb.call(ctx); }
if (_resolve) { _resolve(ctx); }//觸發promise的then回撥
});
if (!pending) {//是否執行重新整理callback佇列
pending = true;
timerFunc();
}
//如果沒有傳遞迴調函式,並且當前瀏覽器支援promise,使用promise實現
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
})();
我在註釋中解釋了nextTick()函式的邏輯
上面處理回撥的三個方式的使用優先順序的原因:因為Promise和MutationObserver和觸發的事件在同一個事件迴圈裡面(只不過是執行在微觀佇列裡面),但是setTimeout的回撥函式是執行在下次時間迴圈裡面。
優先使用Promise的原因是MutationObserver在ios9.3.3以上版本的UIWebview中執行一段時間後就停止了。
上面程式碼的註釋已經完全說明了程式碼邏輯。簡單理解:將callback 推到佇列裡面,如果還沒有執行過在下次事件迴圈執行觸發callback函式。
注意: 如果使用nextTick()不設定回撥函式,而是使用Promise的方式設定回撥函式,裡面this並不是指向當前的Vue例項,而是指向window(嚴格模式是undefined);
但是通過上面的分析可知:執行上下文是通過Promise.then()裡的回撥函式的第一個引數傳遞的。
nextTick()被使用的地方
1、他是全域性Vue的一個函式,因此我們可以通過vue直接呼叫。
2、Vue系統中,用於處理dom更新的操作
Vue中有一個watcher,用於觀察資料的變化,然後更新dom。前面我們就知道Vue裡面不是每一次資料改變都會觸發更新dom,而是將這些操作都快取在一個佇列,在一個事件迴圈結束之後,重新整理佇列,統一執行dom更新操作。
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 >= 0 && queue[i].id > watcher.id) {
i--;
}
queue.splice(Math.max(i, index) + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}
簡單說明上面程式碼的邏輯,因為是watcher那裡的程式碼,以後會分析到。這裡nextTick()的作用,是在此次事件迴圈結尾的時候重新整理watcher檢查的dom更新操作。
3、區域性Vue觸發$nextTick(),在dom更新後執行相應邏輯。
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)// 設定nextTick回撥函式的上下文環境是當前Vue例項
};
上面是renderMinxin中的一段程式碼,也就是render模組初始化的程式碼。
總結
如果不瞭解它的程式碼,我們會產生理解誤區。
1、nextTick()並不會重繪當前頁面,並且它也不是在頁面重繪才執行,而是在此次事件迴圈結束後一定會執行的。
2、此方法的觸發並不是在頁面更新完成才執行,第一條已經說了,但是為什麼能在此方法中取到更新後的資料,那是因為dom元素的屬性已經在watcher執行flush佇列的時候改變了,因此是可以在此時獲取的。
證明上述觀點的例項:
h5有一個方法requestFrameAnimation(callback),此方法的回撥是在頁面重繪之前呼叫。通過實驗,更新dom,nextTick()在此方法之前執行。