深刻理解Promise運行原理

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

1.什麼是Promise

Promise能夠認爲是一種用來解決異步處理的代碼規範。常見的異步處理是使用回調函數,回調函數有兩種模式,同步的回調和異步的回調。通常回調函數指的是異步的回調。html

同步回調java

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 afterweb

異步回調api

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: 3promise

然而回調函數有個著名的坑就是「callback hell」,好比:app

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的寫法,只要寫成以下方式就行。ide

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
相關文章
相關標籤/搜索