Promise入門

EliotWang發表於2018-03-09

Promise入門

眾所周知的,Javascript是一種單執行緒的語言,所有的程式碼必須按照所謂的“自上而下”的順序來執行。本特性帶來的問題就是,一些將來的、未知的操作,必須非同步實現(關於非同步,我會在另一篇文章裡進行討論)。本文將討論一個比較常見的非同步解決方案——Promise,時至本文最後更新的日子,Promise的應用已經極其廣泛。

Promise解決的問題

我相信每個前端都遇到過這樣一個問題,當一個非同步任務的執行需要依賴另一個非同步任務的結果時,我們一般會將兩個非同步任務巢狀起來,這種情況發生一兩次還可以忍,但是發生很多次之後,你的程式碼就會變成這個熊樣:

	async1(function(){
		async2(function(){
			async3(function(
				async4(funciton(){
					async5(function(){
						//(╯°□°)╯︵┻━┻
						//...
					});
				});
			));	
		});
	});
複製程式碼

這就是所謂的回撥地獄,程式碼層層巢狀,環環相扣,很明顯,邏輯稍微複雜一些,這樣的程式就會變得難以維護。

對於這種情況,程式設計師們想了很多解決方案(比如將程式碼模組化),但流程控制上,還是沒有掏出})的大量巢狀。但去年ES2015的標準裡,Promise的標準化,一定程度上解決了JavaScript的流程操作問題。

Promise的基本用法

時至今日,很多現代瀏覽器都已經實現,但是為了相容,建議自行對Promise進行封裝或者使用第三方的解決方案(如webpack對es6語法進行編譯)。 那麼,我麼將得到一個Promise建構函式,新建一個Promise的例項:

	var _promise = new Promise(function(resolve, reject){
		setTimeout(function(){
			var rand = Math.random();
			if(rand<0.5){
				resolve("resolve" + rand);
			}else{
				reject("reject" + rand);
			}
		},1000);
		
	});
	
	/*執行結果:
	 *有兩種情況:
	 *1)無事發生
	 *2)報錯形如:d.js:7 Uncaught (in promise) reject0.9541820247347901
	 */
	
複製程式碼

由上所示,Promise的建構函式接收一個函式作為引數,該函式接受兩個額外的函式,resolve和reject,這兩個函式分別代表將當前Promise置為fulfilled(解決)和rejected(拒絕)兩個狀態。Promise正是通過這兩個狀態來控制非同步操作的結果。接下來我們將討論Promise的用法,實際上Promise上的例項_promise是一個物件,不是一個函式。在宣告的時候,Promise傳遞的引數函式會立即執行,因此Promise使用的正確姿勢是在其外層再包裹一層函式。

	
	var run = function(){
		var _promise = new Promise(function(resolve, reject){
			setTimeout(function(){
				var rand = Math.random();
				if(rand<0.5){
					resolve("resolve" + rand);
				}else{
					reject("reject" + rand);
				}
			},1000);
		});
		return _promise;
	}
	run();
複製程式碼

這是Promise的正常用法,接下來,就是對非同步操作結果的處理,接著上面建立的函式run()

	run().then(function(data){
		console.log(data);
	});
複製程式碼

每個Promise的例項物件,都有一個then的方法,這個方法就是用來處理之前各種非同步邏輯的結果。

那麼, 各位可能會問, 這麼做有什麼卵用?

當然有用,到目前為止,我們學會了Promise的基本流程,但是這種用法和巢狀回撥函式似乎沒什麼區別,而且增加了複雜度。但是我們說了,Promise的用處,實際上是在於多重非同步操作相互依賴的情況下,對於邏輯流程的控制。Promise正是通過對兩種狀態的控制,以此來解決流程的控制。請看如下程式碼:

	run().then(function(data){
		//處理resolve的程式碼
		cosnole.log("Promise被置為resolve",data);
	},function(data){
		//處理reject的程式碼
		cosnole.log("程式被置為了reject",data);
	})
複製程式碼

如果非同步操作獲得了我們想要的結果,那我們將呼叫resolve函式,在then的第一個作為引數的匿名函式中可以獲取資料,如果我們得到了錯誤的結果,呼叫reject函式,在then函式的第二個作為引數的匿名函式中獲取錯誤處理資料。 這樣,一個次完整的Promise呼叫就結束了。對於Promise的then()方法,then總是會返回一個Promise例項,因此你可以一直呼叫then,形如run().then().then().then().then().then()..... 在一個then()方法呼叫非同步處理成功的狀態時,你既可以return一個確定的“值”,也可以再次返回一個Promise例項,當返回的是一個確切的值的時候,then會將這個確切的值傳入一個預設的Promise例項,並且這個Promise例項會立即置為fulfilled狀態,以供接下來的then方法裡使用。如下所示:

	run().then(function(data){
		console.log("第一次",data);
		return data;
	}).then(function(data){
		console.log("第二次",data);
		return data;
	}).then(function(data){
		console.log("第三次",data);
		return data;
	});
	/* 非同步處理成功的列印結果:
		第一次 resolve0.49040459200760167d.js:18 
		第二次 resolve0.49040459200760167d.js:21 
		第三次 resolve0.49040459200760167
		由此可知then方法可以無限呼叫下去。
	*/
複製程式碼

根據這個特性,我們就可以將相互依賴的多個非同步邏輯,進行比較順序的管理了。下面舉一個擁有3個非同步操作的例子,程式碼有些長。

	//第一個非同步任務
	function run_a(){
		return new Promise(function(resolve, reject){
			//假設已經進行了非同步操作,並且獲得了資料
			resolve("step1");
		});
	}
	//第二個非同步任務
	function run_b(data_a){
		return new Promise(function(resolve, reject){
			//假設已經進行了非同步操作,並且獲得了資料
			console.log(data_a);
			resolve("step2");
		});
	}
	//第三個非同步任務
	function run_c(data_b){
		return new Promise(function(resolve, reject){
			//假設已經進行了非同步操作,並且獲得了資料
			console.log(data_b);
			resolve("step3");
		});
	}
	
	//連續呼叫
	run_a().then(function(data){
		return run_b(data);
	}).then(function(data){
		return run_c(data);
	}).then(function(data){
		console.log(data);
	});
	
	/*執行結果
	  step1
	  step2
	  step3
	*/
	
複製程式碼

這樣,連續依賴的幾個非同步操作,就完成了,解決了讓人頭痛的回撥地獄問題。

非同步操作拒絕及終止呼叫鏈

前文提到過,then方法可以接收兩個匿名函式作為引數,第一個引數是Promise置為fulfilled狀態後的回撥,第二個是置為rejected狀態的回撥。在很多情況下,如果連續的幾個非同步任務,其中某個非同步任務處理失敗,那麼接下來的幾個任務很大程度上就不需要繼續處理了,那麼我們該如何終止then的呼叫鏈呢?在Promsie的例項上,除了then方法外,還有一個catch方法,catch方法的具體作用,我們沿用上面的程式碼,將run_a()改造一下來看:

	//修改run_a的一步操作可能存在拒絕狀態
	function run_a(){
		return new Promise(function(resolve, reject){
			setTimeout(function(){
				if(Math.random()>.5){
					resolve("step1");
				}else{
					reject("error");
				}
			},1000);
		});
	}
	
	//這樣做不會終止
	run_a().then(function(data){
		return run_b(data);
	},function(data){
		//如果是這樣處理rejected狀態,並不會終止呼叫鏈
		return data;
	}).then(function(data){
		return run_c(data);
	}).then(function(data){
		console.log(data);
	});
	
	//在呼叫鏈的末尾加上catch方法,當某個環節的Promise的非同步處理出錯時,將終止其後的呼叫,直接跳到最後的catch
	run_a().then(function(data){
		return run_b(data);
	}).then(function(data){
		return run_c(data);
	}).then(function(data){
		console.log(data);
	}).catch(function(e){
		//rejected的狀態將直接跳到catch裡,剩下的呼叫不會再繼續
		console.log(e);
	});
複製程式碼

以上程式碼簡單描述瞭如何終止鏈式呼叫,值得注意的是,catch方法還有try catch的作用,也就是說,then裡面的邏輯程式碼如果出現了錯誤,並不會在控制檯丟擲,而是會直接有catch捕獲。

ES6對Promise/A+的擴充套件

Promise的提出的時間很早,其標準稱為Promise/A+,內容不多,ES6將Promise寫入了標準,並在其基礎上進行了擴充套件,具體可以參考:

· 由malcolm yud對Promise/A+的翻譯 · 阮一峰ES6入門—Promise

這裡將講一下ES6對Promise標準的擴充套件,也可以直接看上面的參考連結

Promise.all的擴充套件


本擴充套件實現了將多個非同步操作合併為一個操作,也就是並行處理非同步,最後統一操作結果,注意:本方法只能通過Promise物件直接呼叫,例項不能進行此操作。

all()接收一個引數陣列,陣列中的每一項都對應一個

	//第一個非同步任務
	function run_a(){
		return new Promise(function(resolve, reject){                
			//假設已經進行了非同步操作,並且獲得了資料
			resolve("step1")
		});
	}
	//第二個非同步任務
	function run_b(){
		return new Promise(function(resolve, reject){
			//假設已經進行了非同步操作,並且獲得了資料
			resolve("step2");
		});
	}
	//第三個非同步任務
	function run_c(){
		return new Promise(function(resolve, reject){
			//假設已經進行了非同步操作,並且獲得了資料
			resolve("step3");
		});
	}
	
	Promise.all([run_a(),run_b(),run_c()]).then(function(data){
		console.log(data);
	},function(data){
		console.log(data);
	});
	/*列印結果
	  ["step1","step2","step3"]
	*/
	
	//修改第二個非同步任務
	//第一個非同步任務
	function run_b(){
		return new Promise(function(resolve, reject){                
			//假設已經進行了非同步操作,並且獲得了資料
			reject("step2")
		});
	}
	/*列印結果
	 *捕獲了第一個出現的拒絕狀態的資料
	  ["step2"]
	*/
複製程式碼

由上所示,並行運算的結果將按照入參順序放在放在陣列裡返回。

Promise.race的擴充套件


race本意為賽跑,顧名思義,race的用法就是並列的幾個非同步操作,誰先處理結束就以誰為準。

	//第一個非同步任務
	function run_a(){
		return new Promise(function(resolve, reject){
			setTimeout(function(){
                console.log("執行到step1");                
                resolve("step1")
			},3000);
		});
	}
	//第二個非同步任務
	function run_b(){
		return new Promise(function(resolve, reject){
            setTimeout(function(){
                console.log("執行到step2");
                resolve("step2");
            },1000);
		});
	}
	//第三個非同步任務
	function run_c(){
		return new Promise(function(resolve, reject){
            setTimeout(function(){
                console.log("執行到step3");
                resolve("step3");
            },3000);
		});
	}

    Promise.race([run_a(),run_b(),run_c()]).then(function(results){
        console.log(results);
    },function(data){
        console.log(data);
    });
    /*
    	列印結果: 
    	執行到step2
		step2
		執行到step1
 		執行到step3
    */
複製程式碼

可以看出,run_b先執行完,進入了then函式進行回撥,但需要注意的是,第一個結束的非同步操作回撥後,其它的非同步操作還會繼續執行,只是並不會繼續進入then了而已。

結語

以上就是對ES6標準下的Promise的簡單理解,Promise出現的時間不短,在很多開源專案裡被廣泛應用,理解了Promise,有助於對程式碼的進一步理解。本篇文章作為我個人的學習記錄,希望起到拋磚引玉的作用,對大家的學習起到點點作用。

相關文章