原始碼解讀-vue是如何實現$nextTick的
阿新 • • 發佈:2020-12-29
技術標籤:原始碼解讀
前言:
本文需要一定的事件迴圈相關知識,想了解事件迴圈的小夥伴可以看
這裡。
本文要弄明白下面兩件事:
$nextTick
什麼時候執行vue
中nextTick
與$nextTick
區別
1.檢視原始碼中的$nextTick
方法
Vue.prototype.$nextTick = function(fn) {
return nextTick(fn, this)
};
可以看到$nextTick
呼叫的也是nextTick
方法,只不過$nextTick
預設綁定了this
上下文,也就是Vue
例項物件
2.下面檢視nextTick方法
function nextTick(cb, ctx) { var _resolve; callbacks.push(function() { 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 if (!cb && typeof Promise !== 'undefined') { return new Promise(function(resolve) { _resolve = resolve; }) } }
callbacks
一個非同步佇列,傳入的回撥函式會被儲存在這個陣列內,等待時機執行if (!cb && typeof Promise !== 'undefined')
如果沒有回撥方法,並且當前環境支援Promise
,那麼nextTick
返回的是一個Promise
物件
3.檢視timerFunc方法
if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); timerFunc = function() { p.then(flushCallbacks); //ios的UIWebViews中,回撥推送到微任務佇列後不會立即重新整理,通過新增空定時器來強制重新整理微任務佇列 if (isIOS) { setTimeout(noop); } }; isUsingMicroTask = true; } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { //如果支援MutationObserver var counter = 1; var observer = new MutationObserver(flushCallbacks); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function() { counter = (counter + 1) % 2; textNode.data = String(counter); }; isUsingMicroTask = true; } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // but it is still a better choice than setTimeout. timerFunc = function() { setImmediate(flushCallbacks); }; } else { // Fallback to setTimeout. timerFunc = function() { setTimeout(flushCallbacks, 0); }; }
經過一系列的判斷方法,用來判斷當前執行環境到底支援哪種方法,可以看到最後timerFunc
執行的都是flushCallbacks
方法。
4.flushCallbacks方法
function flushCallbacks() {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
這個方法執行時,會將回調佇列做一個淺拷貝,並且初始化這個佇列,防止影響下次事件迴圈,接下來將淺拷貝後的陣列進行迴圈並執行。
到這步,$nextTick方法就算執行完畢了。
總結:
nextTick
與$nextTick
方法基本一致,$nextTick
方法預設繫結vue
例項為上下文。nextTick
上下文需要傳入,若不傳入則預設繫結為window
。
$nextTick
方法執行時會判斷當前執行環境是否支援Promise
若支援則放入Promise.then()
內,若不支援則判斷是否支援MutationObserver
,如果不支援的話則會判斷是否支援setImmediate
方法,否則的話會加入setTimeout
中。
一次事件迴圈(event loop
)的過程
巨集任務 => 所有微任務 => ui渲染
其中 Promise.then
以及MutationObserver
為微任務,在當前事件迴圈執行。
setImmediate
、setTimeout
為巨集任務,在下次事件迴圈執行。
下面程式碼為vue
原始碼中的nextTick
相關程式碼,我做了部分註釋。
var isUsingMicroTask = false; //是否使用MutationObserver來觸發回撥函式執行,另作他用,可以在原始碼中搜索用到的地方,本文不做深究
var callbacks = []; //儲存回撥函式
var pending = false; //此次nextTick是否執行中的標記
var timerFunc; //觸發方法
function noop(a, b, c) {} //空函式,ios用來強制重新整理微任務佇列
//執行回撥佇列
function flushCallbacks() {
pending = false; //執行中標記置否
var copies = callbacks.slice(0); //淺拷貝回撥
callbacks.length = 0; // 清空陣列
for (var i = 0; i < copies.length; i++) {
copies[i](); //執行儲存的函式
}
}
//判斷當前環境是否支援Promise
//Vue 在內部對非同步佇列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,
//如果執行環境不支援,則會採用 setTimeout(fn, 0) 代替。
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function() {
p.then(flushCallbacks); //將flushCallbacks放入微任務
//ios的UIWebViews中,回撥推送到微任務佇列後不會立即重新整理,通過新增空定時器來強制重新整理微任務佇列
if (isIOS) {
setTimeout(noop);
}
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
//如果支援MutationObserver
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function() {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// but it is still a better choice than setTimeout.
timerFunc = function() {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = function() {
setTimeout(flushCallbacks, 0);
};
}
function nextTick(cb, ctx) {
var _resolve;
callbacks.push(function() { //將函式加入回撥陣列中,函式執行時會自動執行傳入的回撥函式
if (cb) {
try {
cb.call(ctx); //更改this為傳入的ctx,並執行,$nextTick為vue。nextTick為傳入的上下文,如果沒傳則this為window
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc(); //執行函式
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function(resolve) {
_resolve = resolve;
})
}
}
Vue.prototype.$nextTick = function(fn) {
return nextTick(fn, this)
};
如果想獲取更多內容,可以掃描下方二維碼,一起學習,一起進步。