原始碼解讀-vue是如何實現$nextTick的

左道前端發表於2020-12-28

前言:

本文需要一定的事件迴圈相關知識,想了解事件迴圈的小夥伴可以看
這裡

本文要弄明白下面兩件事:

  • $nextTick什麼時候執行
  • vuenextTick$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為微任務,在當前事件迴圈執行。

setImmediatesetTimeout為巨集任務,在下次事件迴圈執行。

下面程式碼為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)
};

如果想獲取更多內容,可以掃描下方二維碼,一起學習,一起進步。
左道前端

相關文章