手寫promise,瞭解一下(二)

風光好採光發表於2018-08-06

支援鏈式操作

我們平時寫promise一般都是對應的一組流程化的操作,如這樣: promise.then(f1).then(f2).then(f3) 但是我們之前的版本最多隻能註冊一個回撥,這一節我們就來實現鏈式操作。

目標

使promise支援鏈式操作

實現

想支援鏈式操作,其實很簡單,首先儲存回撥時要改為使用陣列

self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];
複製程式碼

當然執行回撥時,也要改成遍歷回撥陣列執行回撥函式

self.onFulfilledCallbacks.forEach((callback) => callback(self.value));
複製程式碼

最後,then方法也要改一下,只需要在最後一行加一個return this即可,這其實和jQuery鏈式操作的原理一致,每次呼叫完方法都返回自身例項,後面的方法也是例項的方法,所以可以繼續執行。

Promise.prototype.then = function(onFulfilled, onRejected){
	let self = this;

	if(self.state === 'resolved'){
		onFulfilled(self.value);
	}

	if(self.state === 'rejected'){
		onRejected(self.reson);
	}

	if(self.state === 'padding'){
		self.onResolvedCallbacks.push(function(){
			onFulfilled(self.value);
		});
		self.onRejectedCallbacks.push(function(){
			onRejected(self.reson);
		});
	}
	
	return this;
}
複製程式碼

這種簡單返回this會有一個問題,這裡為方便講解我們引入一個常見場景:用promise順序讀取檔案內容,場景程式碼如下:

let p = new Promise((resolve, reject) => {
    fs.readFile('../file/1.txt', "utf8", function(err, data) {
        err ? reject(err) : resolve(data)
    });
});
let f1 = function(data) {
    console.log(data)
    return new Promise((resolve, reject) => {
        fs.readFile('../file/2.txt', "utf8", function(err, data) {
            err ? reject(err) : resolve(data)
        });
    });
}
let f2 = function(data) {
    console.log(data)
    return new Promise((resolve, reject) => {
        fs.readFile('../file/3.txt', "utf8", function(err, data) {
            err ? reject(err) : resolve(data)
        });
    });
}
let f3 = function(data) {
    console.log(data);
}
let errorLog = function(error) {
    console.log(error)
}
p.then(f1).then(f2).then(f3).catch(errorLog)
複製程式碼

上面場景,我們讀取完1.txt後並列印1.txt內容,再去讀取2.txt並列印2.txt內容,再去讀取3.txt並列印3.txt內容,而讀取檔案都是非同步操作,所以都是返回一個promise,我們上一節實現的promise可以實現執行完非同步操作後執行後續回撥,但是本節的回撥讀取檔案內容操作並不是同步的,而是非同步的,所以當讀取完1.txt後,執行它回撥onFulfilledCallbacks裡面的f1,f2,f3時,非同步操作還沒有完成,所以我們本想得到這樣的輸出:

this is 1.txt
this is 2.txt
this is 3.txt
複製程式碼

但是實際上卻會輸出

this is 1.txt
this is 1.txt
this is 1.txt
複製程式碼

所以要想實現非同步操作序列,我們不能將回撥函式都註冊在初始promise的onFulfilledCallbacks裡面,而要將每個回撥函式註冊在對應的非同步操作promise的onFulfilledCallbacks裡面,用讀取檔案的場景來舉例,f1要在p的onFulfilledCallbacks裡面,而f2應該在f1裡面return的那個Promise的onFulfilledCallbacks裡面,因為只有這樣才能實現讀取完2.txt後才去列印2.txt的結果。

但是,我們平常寫promise一般都是這樣寫的: promise.then(f1).then(f2).then(f3),一開始所有流程我們就指定好了,而不是在f1裡面才去註冊f1的回撥,f2裡面才去註冊f2的回撥。

如何既能保持這種鏈式寫法的同時又能使非同步操作銜接執行呢?我們其實讓then方法最後不再返回自身例項,而是返回一個新的promise即可,我們可以叫它bridgePromise,它最大的作用就是銜接後續操作,我們看下具體實現程式碼:

function Promise(executor){
	let self = this;
	self.state = 'padding';
	self.value = undefined;
	self.reason = undefined;
	self.onResolvedCallbacks = [];
	self.onRejectedCallbacks = [];

	function resolve(value){
		if(self.state == 'padding'){
			self.state = 'resolved'
			self.value = value
			self.onResolvedCallbacks.forEach(fn=>fn());
		}	
	}

	function reject(reason){
		if(self.state == 'padding'){
			self.state = 'rejected'
			self.reason = reason
			self.onRejectedCallbacks.forEach(fn=>fn());
		}
	}

	try{
		executor(resolve, reject);
	}catch(e){
		reject(e);
	}
}

Promise.prototype.then = function(onFulfilled, onRejected){
	let self = this;
	//不能寫成:let promise2 = new Promise(function(resolve, reject){}}
	let promise2;
	promise2 = new Promise(function(resolve, reject){
		if(self.state === 'resolved'){
			try{
				let x = onFulfilled(self.value);
				resolvePromise(promise2, x, resolve, reject);
			}catch(e){
				reject(e);
			}
		}

		if(self.state === 'rejected'){
			try{
				let x = onRejected(self.reason);
				resolvePromise(promise2, x, resolve, reject);
			}catch(e){
				reject(e);
			}
		}

		if(self.state === 'padding'){
			self.onResolvedCallbacks.push(function(){
				try{
					let x = onFulfilled(self.value);
					resolvePromise(promise2, x, resolve, reject);
				}catch(e){
					reject(e);
				}
			});
			self.onRejectedCallbacks.push(function(){
				try{
					let x = onRejected(self.reason);
					resolvePromise(promise2, x, resolve, reject);
				}catch(e){
					reject(e);
				}
			});
		}
	});

	return promise2;	
}


/**
 * [resolvePromise description]
 * @param  {[type]} promise2 [then的返回值(新的promise)]
 * @param  {[type]} x        [then種成功或失敗的返回值]
 * @param  {[type]} resolve  [promise2中的resolve]
 * @param  {[type]} reject   [promise2中的reject]
 * @return {[type]}          [description]
 */
function resolvePromise(promise2, x, resolve, reject){
	//console.log('resolvePromise');
	if(promise2 === x){
		return reject(new TypeError('迴圈引用promise'));
	}

	if(x!==null && (typeof x === 'object' || typeof x === 'function')){
		let then = x.then;
		if(typeof then === 'function'){//說明返回的是promise,只有promise上才有then
			console.log('function');
			then.call(
				x, 
				y=>resolve(y), 
				err=>reject(err)
			);
		}else{//說明返回的是 普通物件
			console.log('object');
			resolve(x);
		}
	}else{//說明返回的是 普通值
		resolve(x);
	}
}
複製程式碼

這裡很抽象,我們還是以檔案順序讀取的場景畫一張圖解釋一下流程:

當執行p.then(f1).then(f2).then(f3)時:

  1. 先執行p.then(f1)返回了一個bridgePromise(p2),並在p的onFulfilledCallbacks回撥列表中放入一個回撥函式,回撥函式負責執行f1並且更新p2的狀態.
  2. 然後.then(f2)時返回了一個bridgePromise(p3),這裡注意其實是p2.then(f2),因為p.then(f1)時返回了p2。此時在p2的onFulfilledCallbacks回撥列表中放入一個回撥函式,回撥函式負責執行f2並且更新p3的狀態.
  3. 然後.then(f3)時返回了一個bridgePromise(p4),並在p3的onFulfilledCallbacks回撥列表中放入一個回撥函式,回撥函式負責執行f3並且更新p4的狀態. 到此,回撥關係註冊完了,如圖所示:

手寫promise,瞭解一下(二)

  1. 然後過了一段時間,p裡面的非同步操作執行完了,讀取到了1.txt的內容,開始執行p的回撥函式,回撥函式執行f1,列印出1.txt的內容“this is 1.txt”,並將f1的返回值放到resolvePromise中開始解析。resolvePromise一看傳入了一個promise物件,promise是非同步的啊,得等著呢,於是就在這個promise物件的then方法中繼續resolvePromise這個promise物件resolve的結果,一看不是promise物件了,而是一個具體值“this is 2.txt”,於是呼叫bridgePromise(p2)的reslove方法將bridgePromise(p2)的狀態更新為fulfilled,並將“this is 2.txt”傳入p2的回撥函式中去執行。

  2. p2的回撥開始執行,f2拿到傳過來的“this is 2.txt”引數開始執行,列印出2.txt的內容,並將f2的返回值放到resolvePromise中開始解析,resolvePromise一看傳入了一個promise物件,promise是非同步的啊,又得等著呢........後續操作就是不斷的重複4,5步直到結束。

相關文章