深入理解Promise執行原理

鈞嘢嘢發表於2018-01-17

本文大多數內容翻譯自該篇文章

1.什麼是Promise

Promise可以認為是一種用來解決非同步處理的程式碼規範。常見的非同步處理是使用回撥函式,回撥函式有兩種模式,同步的回撥和非同步的回撥。一般回撥函式指的是非同步的回撥。

同步回撥

	function add(a, b, callback) { callback(a + b) }
        
        console.log('before');
        add(1, 2, result => console.log('Result: ' + result);
        console.log('after');
複製程式碼

輸出結果為: before Result:3 after

非同步回撥

    function addAsync(a, b, callback) {
        setTimeout( () => callback(a + b), 1000);
    }

    console.log('before');
    addAsync(1, 2, result => console.log('Result: ' + result));
    console.log('after');
複製程式碼

輸出結果: before after Result: 3

然而回撥函式有個著名的坑就是“callback hell”,比如:

	doSomething1(function(value1) {
		doSomething2(function(value2) {
			doSomething3(function(value3) {
				console.log("done! The values are: " + [value1, value2, value3].join(','));
			})
		})
	})
複製程式碼

為了等value1, value2, value3資料都準備好,必須要一層一層巢狀回撥函式。如果一直巢狀下去,就形成了callback hell,不利於程式碼的閱讀。

如果改用Promise的寫法,只要寫成如下方式就行。

	doSomething1().then(function() {
		return value1;
	}).then(function(tempValue1) {
		return [tempValue1, value2].join(',');		
	}).then(function(tempValue2) {
		console.log("done! ", [tempValue2, value3].join(','));
	});
複製程式碼

可以注意到,Promise實際上是把回撥函式從doSomething函式中提取到了後面的then方法裡面,從而防止多重巢狀的問題。

一個 Promise 物件代表一個目前還不可用,但是在未來的某個時間點可以被解析的值。它要麼解析成功,要麼失敗丟擲異常。它允許你以一種同步的方式編寫非同步程式碼。

Promise的實現是根據Promises/A+規範實現的。

2.Promise物件和狀態

對於Promise的基本使用和入門,可以參考promise-book。這裡對Promise的使用做了比較詳細的介紹。

2.1 resolve & reject

Promise建構函式用來構造一個Promise物件,其中入參匿名函式中resolvereject這兩個也都是函式。如果resolve執行了,則觸發promise.then中成功的回撥函式;如果reject執行了,則觸發promise.then中拒絕的回撥函式。

	var promise = new Promise(function(resolve, reject) {
		// IF 如果符合預期條件,呼叫resolve
		resolve('success');

		// ELSE 如果不符合預期條件,呼叫reject
		reject('failure')
	})
複製程式碼

2.2 Fulfilled & Rejected

Promise物件一開始的值是Pending準備狀態。

執行了resolve()後,該Promise物件的狀態值變為onFulfilled狀態。

執行了reject()後,該Promise物件的狀態值變為onRejected狀態。

Promise物件的狀態值一旦確定(onFulfilled或onRejected),就不會再改變。即不會從onFulfilled轉為onRejected,或者從onRejected轉為onFulfilled。

2.3 快捷方法

獲取一個onFulfilled狀態的Promise物件:

Promise.resolve(1);

// 等價於

new Promise((resolve) => resolve(1));
複製程式碼

獲取一個onRejected狀態的Promise物件:

Promise.reject(new Error("BOOM")) 

// 等價於

new Promise((resolve, reject) 
	=> reject(new Error("BOOM")));
複製程式碼

更多快捷方法請參考Promise API

3.異常捕獲:then和catch

Promise的異常捕獲有兩種方式:

  1. then匿名函式中的reject方法
  2. catch方法

3.1 then中的reject方法捕獲異常

這種方法只能捕獲前一個Promise物件中的異常,即呼叫then函式的Promise物件中出現的異常。

	var promise = Promise.resolve();

	promise.then(function() {
	    throw new Error("BOOM!")
	}).then(function (success) {
	    console.log(success);
	}, function (error) {
		// 捕捉的是第一個then返回的Promise物件的錯誤
	    console.log(error);
	});
複製程式碼

但該種方法無法捕捉當前Promise物件的異常,如:

	var promise = Promise.resolve();

	promise.then(function() {
	    return 'success';
	}).then(function (success) {
	    console.log(success);
		throw new Error("Another BOOM!");
	}, function (error) {
	    console.log(error);  // 無法捕捉當前then中丟擲的異常
	});
複製程式碼

3.2 catch捕獲異常

上述栗子若改寫成如下形式,最後追加一個catch函式,則可以正常捕捉到異常。

	var promise = Promise.resolve();

	promise.then(function() {
	    return 'success';
	}).then(function (success) {
	    console.log(success);
		throw new Error("Another BOOM!");
	}).catch(function (error) {
        console.log(error); // 可以正常捕捉到異常
    });
複製程式碼

catch方法可以捕獲到then中丟擲的錯誤,也能捕獲前面Promise丟擲的錯誤。 因此建議都通過catch方法捕捉異常。

	var promise = Promise.reject("BOOM!");

	promise.then(function() {
	    return 'success';
	}).then(function (success) {
	    console.log(success);
		throw new Error("Another BOOM!");
	}).catch(function (error) {
        console.log(error);  // BOOM!
    });
複製程式碼

值得注意的是:catch方法其實等價於then(null, reject),上面可以寫成:

	promise.then(function() {
		return 'success';
	}).then(function (success) {
	    console.log(success);
		throw new Error("Another BOOM!");
	}).then(null, function(error) {
		console.log(error);
	})
複製程式碼

總結來說就是:

  1. 使用promise.then(onFulfilled, onRejected)的話,在 onFulfilled中發生異常的話,在onRejected中是捕獲不到這個異常的。

  2. promise.then(onFulfilled).catch(onRejected)的情況下then中產生的異常能在.catch中捕獲

  3. .then.catch在本質上是沒有區別的需要分場合使用。

4.動手逐步實現Promise

瞭解一個東西最好的方式就是嘗試自己實現它,儘管可能很多地方不完整,但對理解內在的執行原理是很有幫助的。

這裡主要引用了JavaScript Promises ... In Wicked Detail這篇文章的實現,以下內容主要是對該篇文章的翻譯。

4.1 初步實現

首先實現一個簡單的Promise物件型別。只包含最基本的then方法和resolve方法,reject方法暫時不考慮。

function Promise(fn) {
	// 設定回撥函式
	var callback = null;

	// 設定then方法
	this.then = function (cb) {
		callback = cb;
	};

	// 定義resolve方法
	function resolve(value) {
		// 這裡強制resolve的執行在下一個Event Loop中執行
		// 即在呼叫了then方法後設定完callback函式,不然callback為null
		setTimeout(function () {
			callback(value);
		}, 1);
	}

	// 執行new Promise時傳入的函式,入參是resolve
	// 按照之前講述的,傳入的匿名函式有兩個方法,resolve和reject
	fn(resolve);
}

function doSomething() {
	return new Promise(function (resolve) {
		var value = 42;
		resolve(value);
	});
}

// 呼叫自己的Promise
doSomething().then(function (value) {
	console.log("got a value", value);
});
複製程式碼

好了,這是一個很粗略版的Promise。這個實現連Promise需要的三種狀態都還沒實現。這個版本主要直觀展示了Promise的核心方法:thenresolve

該版本如果then非同步呼叫的話,還是會導致Promise中的callback為null。

	var promise = doSomething();
	
	setTimeout(function() {
	    promise.then(function(value) {
	    	console.log("got a value", value);
	})}, 1);
複製程式碼

後續通過加入狀態來維護Promise,就可以解決這種問題。

4.2 Promise新增狀態

通過新增一個欄位state用來維護Promise的狀態,當執行了resolve函式後,修改stateresolved,初始statependding

function Promise(fn) {

	var state = 'pending'; // 維護Promise例項的狀態
	var value;
	var deferred; // 在狀態還處於pending時用於儲存回撥函式的引用

	function resolve(newValue) {
		value = newValue;
		state = 'resolved';

		if (deferred) {
			// deferred 有值表明回撥已經設定了,呼叫handle方法處理回撥函式
			handle(deferred);
		}
	}

	// handle方法通過判斷state選擇如何執行回撥函式
	function handle(onResolved) {
		// 如果還處於pending狀態,則先儲存then傳入的回撥函式
		if (state === 'pending') {
			deferred = onResolved;
			return;
		}

		onResolved(value);
	}

	this.then = function (onResolved) {
		// 對then傳入的回撥函式,呼叫handle去執行回撥函式
		handle(onResolved);
	};

	fn(resolve);
}

function doSomething() {
	return new Promise(function (resolve) {
		var value = 42;
		resolve(value);
	});
}

doSomething().then(function (value) {
	console.log("got a value", value);
});
複製程式碼

加入了狀態後,可以通過判斷狀態來解決呼叫先後順序的問題:

  • resolve()執行前呼叫then()。表明這時還沒有value處理好,這時的狀態就是pending,此時先保留then()傳入的回撥函式,等呼叫resolve()處理好value值後再執行回撥函式,此時回撥函式儲存在deferred中。

  • resolve()執行後呼叫then()。表明這時value已經通過resolve()處理完成了。當呼叫then()時就可以通過呼叫傳入的回撥函式處理value值。

該版本的Promise我們可以隨意先呼叫resolve()pending(),兩者的順序對程式的執行不會造成影響了。

4.3 Promise新增呼叫鏈

Promise是可以鏈式呼叫的,每次呼叫then()後都返回一個新的Promise例項,因此要修改之前實現的then()方法。

function Promise(fn) {
	var state = 'pending';
	var value;
	var deferred = null;

	function resolve(newValue) {
		value = newValue;
		state = 'resolved';

		if (deferred) {
			handle(deferred);
		}
	}

	// 此時傳入的引數是一個物件
	function handle(handler) {
		if (state === 'pending') {
			deferred = handler;
			return;
		}

		// 如果then沒有傳入回撥函式
		// 則直接執行resolve解析value值
		if (!handler.onResolved) {
			handler.resolve(value);
			return;
		}

		// 獲取前一個then回撥函式中的解析值
		var ret = handler.onResolved(value);
		handler.resolve(ret);
	}

	// 返回一個新的Promise例項
	// 該例項匿名函式中執行handle方法,該方法傳入一個物件
	// 包含了傳入的回撥函式和resolve方法的引用
	this.then = function (onResolved) {
		return new Promise(function (resolve) {
			handle({
				onResolved: onResolved, // 引用上一個Promise例項then傳入的回撥
				resolve: resolve
			});
		});
	};

	fn(resolve);
}

function doSomething() {
	return new Promise(function (resolve) {
		var value = 42;
		resolve(value);
	});
}

// 第一個then的返回值作為第二個then匿名函式的入參
doSomething().then(function (firstResult) {
	console.log("first result", firstResult);
	return 88;
}).then(function (secondResult) {
	console.log("second result", secondResult);
});
複製程式碼

then中是否傳入回撥函式也是可選的,如:

doSomething().then().then(function(result) {
  	console.log('got a result', result);
});
複製程式碼

handle()方法的實現中,如果沒有回撥函式,直接解析已有的value值,該值是上一個Promise例項中呼叫resolve(value)中傳入的。

if(!handler.onResolved) {
  	handler.resolve(value);
  	return;
}
複製程式碼

如果回撥函式中返回的是一個Promise物件而不是一個具體數值怎麼辦?此時我們需要對返回的Promise呼叫then()方法。

	doSomething().then(function(result) {
	  	// doSomethingElse returns a promise
	  	return doSomethingElse(result);
	}).then(function(anotherPromise) {
	  	anotherPromise.then(function(finalResult) {
	    	console.log("the final result is", finalResult);
	  	});
	});
複製程式碼

每次這樣寫很麻煩,我們可以在我們的Promise中的resole()方法內處理掉這種情況。

function resolve(newValue) {
	// 通過判斷是否有then方法判斷其是否是Promise物件
	if (newValue && typeof newValue.then === 'function') {
		// 遞迴執行resolve方法直至解析出值出來, 
		// 通過handler.onResolved(value)解析出值,這裡handler.onResolve就是resolve方法
		newValue.then(resolve);
		return;
	}

	state = 'resolved';
	value = newValue;

	if (deferred) {
		handle(deferred);
	}
}
複製程式碼

4.4 Promise新增reject處理

直至目前為止,已經有了一個比較像樣的Promise了,現在新增一開始忽略的reject()方法,使得我們可以這樣使用Promise。

doSomething().then(function(value) {
  	console.log('Success!', value);
}, function(error) {
  	console.log('Uh oh', error);
});
複製程式碼

實現也很簡單,reject()方法與resolve()方法類似。

function Promise(fn) {
	var state = 'pending';
	var value;
	var deferred = null;

	function resolve(newValue) {
		if (newValue && typeof newValue.then === 'function') {
			newValue.then(resolve, reject);
			return;
		}
		state = 'resolved';
		value = newValue;

		if (deferred) {
			handle(deferred);
		}
	}

	// 新增的reject方法,這裡將Promise例項的狀態設為rejected
	function reject(reason) {
		state = 'rejected';
		value = reason;

		if (deferred) {
			handle(deferred);
		}
	}

	function handle(handler) {
		if (state === 'pending') {
			deferred = handler;
			return;
		}

		var handlerCallback;

		// 新增state對於rejected狀態的判斷
		if (state === 'resolved') {
			handlerCallback = handler.onResolved;
		} else {
			handlerCallback = handler.onRejected;
		}

		if (!handlerCallback) {
			if (state === 'resolved') {
				handler.resolve(value);
			} else {
				handler.reject(value);
			}

			return;
		}

		var ret = handlerCallback(value);
		handler.resolve(ret);
	}

	this.then = function (onResolved, onRejected) {
		return new Promise(function (resolve, reject) {
			handle({
				onResolved: onResolved,
				onRejected: onRejected,
				resolve: resolve,
				reject: reject
			});
		});
	};

	fn(resolve, reject);
}



function doSomething() {
	return new Promise(function (resolve, reject) {
		var reason = "uh oh, something bad happened";
		reject(reason);
	});
}

// 呼叫栗子
doSomething().then(function (firstResult) {
	// wont get in here
	console.log("first result:", firstResult);
}, function (error) {
	console.log("got an error:", error);
});
複製程式碼

目前我們的異常處理機制只能處理自己丟擲的異常資訊,對於其他的一些異常資訊是無法正常捕獲的,如在resolve()方法中丟擲的異常。我們對此做如下修改:

	function resolve(newValue) {
	  	try {
	    	// ... as before
	  	} catch(e) {
	    	     reject(e);
	  	}
	}
複製程式碼

這裡通過新增try catch手動捕獲可能出現的異常,並在catch中呼叫reject()方法進行處理。同樣對於回撥函式,執行時也可能出現異常,也需要做同樣的處理。

	function handle(deferred) {
	  	// ... as before
	
	  	var ret;
	  	try {
	    	    ret = handlerCallback(value);
	  	} catch(e) {
	    	    handler.reject(e);
	    	    return;
	  	}
	
	  	handler.resolve(ret);
	}
複製程式碼

上述完整的演示程式碼請檢視原文作者提供的fiddle

4.4 Promise保證非同步處理

到目前為止,我們的Promise已經實現了基本比較完善的功能了。這裡還有一點需要注意的是,Promise規範提出不管是resolve()還是reject(),執行都必須保持非同步處理。要實現這一點很簡單,只需做如下修改即可:

	function handle(handler) {
	  	if(state === 'pending') {
	    	    deferred = handler;
	    	    return;
	  	}

	  	setTimeout(function() {
	    	    // ... as before
	  	}, 1);
	}
複製程式碼

問題是為什麼要這麼處理?這主要是為了保證程式碼執行流程的一致性和可靠性。考慮如下栗子:

	var promise = doAnOperation();
	invokeSomething();
	promise.then(wrapItAllUp);
	invokeSomethingElse();
複製程式碼

通過程式碼的意圖應該是希望invokeSomething()invokeSomethingElse()都執行完後,再執行回撥函式wrapItAllUp()。如果Promise的resolve()處理不是非同步的話,則執行順序變為invokeSomething() -> wrapItAllUp() -> invokeSomethingElse(),跟預想的產生不一致。

為了保證這種執行順序的一致性,Promise規範要求resolve必須是非同步處理的。

到這一步,我們的Promise基本像模像樣了。當然離真正的Promise還有一段差距,比如缺乏了常用的便捷方法如all(),race()等。不過本例子實現的方法本來就是從理解Promise原理出發的,相信通過該例子對Promise原理會有比較深入的瞭解。

參考

  1. JavaScript Promises ... In Wicked Detail
  2. Promises/A+
  3. promise-book
  4. A quick guide to JavaScript Promises
  5. MDN web docs
  6. Node.js Design Patterns

相關文章