[Javascript] Promise ES6 詳細介紹
一 前言
本文主要對ES6的 Promise
進行一些入門級的介紹。要想學習一個知識點,肯定是從三個方面出發,what、why、how。下面就跟著我一步步學習吧~
二 什麼是Promise
首先是what。那麼什麼是 Promise
呢?
以下是MDN對 Promise
的定義
譯文:Promise物件用於非同步操作,它表示一個尚未完成且預計在未來完成的非同步操作。
那麼什麼是非同步操作?在學習promise之前需要把這個概念搞明白,下面將抽離一章專門介紹。
2.1 同步與非同步
我們知道,JavaScript的執行環境是「單執行緒」。
所謂單執行緒,是指JS引擎中負責解釋和執行JavaScript程式碼的執行緒只有一個,也就是一次只能完成一項任務,這個任務執行完後才能執行下一個,它會「阻塞」其他任務。這個任務可稱為主執行緒。
但實際上還有其他執行緒,如事件觸發執行緒、ajax請求執行緒等。
這也就引發了同步和非同步的問題。
2.1.1 同步
同步模式,即上述所說的單執行緒模式, 一次 只能執行 一個 任務,函式呼叫後需等到函式執行結束,返回執行的結果,才能進行下一個任務。如果這個任務執行的時間較長,就會導致「 執行緒阻塞 」。
/* 例2.1 */ var x = true; while(x); console.log("don't carry out");//不會執行復制程式碼
上面的例子即同步模式,其中的while是一個死迴圈,它會阻塞程序,因此第三句console不會執行。
同步模式比較簡單,也較容易編寫。但問題也顯而易見,如果請求的時間較長,而阻塞了後面程式碼的執行,體驗是很不好的。因此對於一些耗時的操作,非同步模式則是更好的選擇。
2.1.2 非同步
下面就來看看非同步模式。
非同步模式,即與同步模式相反,可以一起執行 多個任務 ,函式呼叫後不會立即返回執行的結果,如果任務A需要等待,可先執行任務B,等到任務A結果返回後再繼續回撥。
最常見的非同步模式就數定時器了,我們來看看以下的例子。
/* 例2.2 */ setTimeout(function() { console.log('taskA, asynchronous'); }, 0); console.log('taskB, synchronize'); //while(true); -------ouput------- taskB, synchronize taskA, asynchronous複製程式碼
我們可以看到,定時器延時的時間明明為0,但taskA還是晚於taskB執行。這是為什麼呢?由於定時器是非同步的, 非同步任務會在當前指令碼的所有同步任務執行完才會執行 。如果同步程式碼中含有死迴圈,即將上例的註釋去掉,那麼這個非同步任務就不會執行,因為同步任務阻塞了程序。
2.1.3 回撥函式
提起非同步,就不得不談談回撥函數了。上例中, setTimeout
裡的 function
便是回撥函式。可以簡單理解為:(執行完)回(來)調(用)的函式。
以下是WikiPedia對於 callback
的定義。
In computer programming, a callback is a piece of executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at some convenient time.
可以看出,回撥函式是一段可執行的程式碼段,它以「引數」的形式傳遞給其他程式碼,在其合適的時間執行這段(回撥函式)的程式碼。
WikiPedia同時提到
The invocation may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback.
也就是說,回撥函式不僅可以用於非同步呼叫,一般同步的場景也可以用回撥。在同步呼叫下,回撥函式一般是最後執行的。而非同步呼叫下,可能一段時間後執行或不執行(未達到執行的條件)。
/* 例2.3 */ /******************同步回撥******************/ var fun1 = function(callback) { //do something console.log("before callback"); (callback && typeof(callback) === 'function') && callback(); console.log("after callback"); } var fun2 = function(param) { //do something var start = new Date(); while((new Date() - start) < 3000) { //delay 3s } console.log("I'm callback"); } fun1(fun2); -------output-------- before callback //after 3s I’m callback after callback複製程式碼
由於是同步回撥,會阻塞後面的程式碼,如果fun2是個死迴圈,後面的程式碼就不執行了。
上一小節中 setTimeout
就是常見的非同步回撥,另外常見的非同步回撥即ajax請求。
/* 例2.4 */ /******************非同步回撥******************/ function request(url, param, successFun, errorFun) { $.ajax({ type: 'GET', url: url, param: param, async: true,//預設為true,即非同步請求;false為同步請求 success: successFun, error: errorFun }); } request('test.html', '', function(data) { //請求成功後的回撥函式,通常是對請求回來的資料進行處理 console.log('請求成功啦, 這是返回的資料:', data); },function(error) { console.log('sorry, 請求失敗了, 這是失敗資訊:', error); });複製程式碼
2.2 為什麼使用Promise
說完了以上基本概念,我們就可以繼續學習 Promise
了。
上面提到, Promise
物件是用於非同步操作的。既然我們可以使用非同步回撥來進行非同步操作,為什麼還要引入一個 Promise
新概念,還要花時間學習它呢?不要著急,下面就來談談 Promise
的過人之處。
我們先看看下面的demo,利用 Promise
改寫例2.4的非同步回撥。
/* 例2.5 */ function sendRequest(url, param) { return new Promise(function (resolve, reject) { request(url, param, resolve, reject); }); } sendRequest('test.html', '').then(function(data) { //非同步操作成功後的回撥 console.log('請求成功啦, 這是返回的資料:', data); }, function(error) { //非同步操作失敗後的回撥 console.log('sorry, 請求失敗了, 這是失敗資訊:', error); });複製程式碼
這麼一看,並沒有什麼區別,還比上面的非同步回撥複雜,得先新建Promise再定義其回撥。其實, Promise
的真正強大之處在於它的多重鏈式呼叫,可以避免層層巢狀回撥。如果我們在第一次ajax請求後,還要用它返回的結果再次請求呢?
/* 例2.6 */ request('test1.html', '', function(data1) { console.log('第一次請求成功, 這是返回的資料:', data1); request('test2.html', data1, function (data2) { console.log('第二次請求成功, 這是返回的資料:', data2); request('test3.html', data2, function (data3) { console.log('第三次請求成功, 這是返回的資料:', data3); //request... 繼續請求 }, function(error3) { console.log('第三次請求失敗, 這是失敗資訊:', error3); }); }, function(error2) { console.log('第二次請求失敗, 這是失敗資訊:', error2); }); }, function(error1) { console.log('第一次請求失敗, 這是失敗資訊:', error1); });複製程式碼
以上出現了多層回撥巢狀,有種暈頭轉向的感覺。這也就是我們常說的厄運回調金字塔(Pyramid of Doom),程式設計體驗十分不好。而使用 Promise
,我們就可以利用 then
進行「鏈式回撥」,將非同步操作以同步操作的流程表示出來。
/* 例2.7 */ sendRequest('test1.html', '').then(function(data1) { console.log('第一次請求成功, 這是返回的資料:', data1); return sendRequest('test2.html', data1); }).then(function(data2) { console.log('第二次請求成功, 這是返回的資料:', data2); return sendRequest('test3.html', data2); }).then(function(data3) { console.log('第三次請求成功, 這是返回的資料:', data3); }).catch(function(error) { //用catch捕捉前面的錯誤 console.log('sorry, 請求失敗了, 這是失敗資訊:', error); });複製程式碼
是不是明顯清晰很多?孰優孰略也無需多說了吧~下面就讓我們真正進入 Promise
的學習。
三 Promise的基本用法
3.1 基本用法
上一小節我們認識了 promise
長什麼樣,但對它用到的 resolve
、 reject
、 then
、 catch
想必還不理解。下面我們一步步學習。
Promise
物件代表一個未完成、但預計將來會完成的操作。
它有以下三種狀態:
pending fulfilled rejected
Promise
有兩種狀態改變的方式,既可以從 pending
轉變為 fulfilled
,也可以從 pending
轉變為 rejected
。一旦狀態改變,就「凝固」了,會一直保持這個狀態,不會再發生變化。當狀態發生變化, promise.then
繫結的函式就會被呼叫。
注意: Promise
一旦新建就會「立即執行」,無法取消。這也是它的缺點之一。
下面就通過例子進一步講解。
/* 例3.1 */ //構建Promise var promise = new Promise(function (resolve, reject) { if (/* 非同步操作成功 */) { resolve(data); } else { /* 非同步操作失敗 */ reject(error); } });複製程式碼
類似構建物件,我們使用 new
來構建一個 Promise
。 Promise
接受一個「函式」作為引數,該函式的兩個引數分別是 resolve
和 reject
。這兩個函式就是就是「回撥函式」,由JavaScript引擎提供。
resolve
函式的作用:在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;
reject
函式的作用:在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
Promise例項生成以後,可以用 then
方法指定 resolved
狀態和 reject
狀態的回撥函式。
/* 接例3.1 */ promise.then(onFulfilled, onRejected); promise.then(function(data) { // do something when success }, function(error) { // do something when failure });複製程式碼
then
方法會返回一個Promise。它有兩個引數,分別為Promise從 pending
變為 fulfilled
和 rejected
時的回撥函式(第二個引數非必選)。這兩個函式都 接受Promise物件傳出的值作為引數 。
簡單來說, then
就是定義 resolve
和 reject
函式的,其 resolve
引數相當於:
function resolveFun(data) { //data為promise傳出的值 }複製程式碼
而新建Promise中的'resolve(data)',則相當於執行resolveFun函式。
Promise新建後就會立即執行。而 then
方法中指定的回撥函式,將 在當前指令碼所有同步任務執行完才會執行 。如下例:
/* 例3.2 */ var promise = new Promise(function(resolve, reject) { console.log('before resolved'); resolve(); console.log('after resolved'); }); promise.then(function() { console.log('resolved'); }); console.log('outer'); -------output------- before resolved after resolved outer resolved複製程式碼
由於 resolve
指定的是非同步操作成功後的回撥函式,它需要等所有同步程式碼執行後才會執行,因此最後列印'resolved',這個和例2.2是一樣的道理。
3.2 基本API
.then()
語法:Promise.prototype.then(onFulfilled, onRejected) 複製程式碼
對promise新增 onFulfilled
和 onRejected
回撥,並返回的是一個新的Promise例項(不是原來那個Promise例項),且返回值將作為引數傳入這個新Promise的 resolve
函式。
因此,我們可以使用鏈式寫法,如上文的例2.7。由於前一個回撥函式,返回的還是一個Promise物件(即有非同步操作),這時後一個回撥函式,就會等待該Promise物件的 狀態發生變化 ,才會被呼叫。
.catch()
語法:Promise.prototype.catch(onRejected) 複製程式碼
該方法是 .then(undefined, onRejected)
的別名,用於指定發生錯誤時的回撥函式。
/* 例3.3 */ promise.then(function(data) { console.log('success'); }).catch(function(error) { console.log('error', error); }); /*******等同於*******/ promise.then(function(data) { console.log('success'); }).then(undefined, function(error) { console.log('error', error); });複製程式碼
/* 例3.4 */ var promise = new Promise(function (resolve, reject) { throw new Error('test'); }); /*******等同於*******/ var promise = new Promise(function (resolve, reject) { reject(new Error('test')); }); //用catch捕獲 promise.catch(function (error) { console.log(error); }); -------output------- Error: test複製程式碼
從上例可以看出, reject
方法的作用,等同於拋錯。
promise物件的錯誤,會一直向後傳遞,直到被捕獲。即錯誤總會被下一個 catch
所捕獲。 then
方法指定的回撥函式,若丟擲錯誤,也會被下一個 catch
捕獲。 catch
中也能拋錯,則需要後面的 catch
來捕獲。
/* 例3.5 */ sendRequest('test.html').then(function(data1) { //do something }).then(function (data2) { //do something }).catch(function (error) { //處理前面三個Promise產生的錯誤 });複製程式碼
上文提到過,promise狀態一旦改變就會凝固,不會再改變。因此promise一旦 fulfilled
了,再拋錯,也不會變為 rejected
,就不會被 catch
了。
/* 例3.6 */ var promise = new Promise(function(resolve, reject) { resolve(); throw 'error'; }); promise.catch(function(e) { console.log(e);//This is never called });複製程式碼
如果沒有使用 catch
方法指定處理錯誤的回撥函式,Promise物件丟擲的錯誤不會傳遞到外層程式碼,即不會有任何反應(Chrome會拋錯),這是Promise的另一個缺點。
/* 例3.7 */ var promise = new Promise(function (resolve, reject) { resolve(x); }); promise.then(function (data) { console.log(data); });複製程式碼
如圖所示,只有Chrome會拋錯,且promise狀態變為 rejected
,Firefox和Safari中錯誤不會被捕獲,也不會傳遞到外層程式碼,最後沒有任何輸出,promise狀態也變為 rejected
。
.all()
語法:Promise.all(iterable) 複製程式碼
該方法用於將多個Promise例項,包裝成一個新的Promise例項。
var p = Promise.all([p1, p2, p3]);複製程式碼
Promise.all
方法接受一個數組(或具有Iterator介面)作引數,陣列中的物件(p1、p2、p3)均為promise例項(如果不是一個promise,該項會被用 Promise.resolve
轉換為一個promise)。它的狀態由這三個promise例項決定。
- 當p1, p2, p3狀態都變為
fulfilled
,p的狀態才會變為fulfilled
,並將三個promise返回的結果,按引數的順序(而不是resolved
的順序)存入陣列,傳給p的回撥函式,如例3.8。 - 當p1, p2, p3其中之一狀態變為
rejected
,p的狀態也會變為rejected
,並把第一個被reject
的promise的返回值,傳給p的回撥函式,如例3.9。
/* 例3.8 */ var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 3000, "first"); }); var p2 = new Promise(function (resolve, reject) { resolve('second'); }); var p3 = new Promise((resolve, reject) => { setTimeout(resolve, 1000, "third"); }); Promise.all([p1, p2, p3]).then(function(values) { console.log(values); }); -------output------- //約 3s 後 ["first", "second", "third"] 複製程式碼
/* 例3.9 */ var p1 = new Promise((resolve, reject) => { setTimeout(resolve, 1000, "one"); }); var p2 = new Promise((resolve, reject) => { setTimeout(reject, 2000, "two"); }); var p3 = new Promise((resolve, reject) => { reject("three"); }); Promise.all([p1, p2, p3]).then(function (value) { console.log('resolve', value); }, function (error) { console.log('reject', error);// => reject three }); -------output------- reject three複製程式碼
這多個 promise 是同時開始、並行執行的,而不是順序執行。從下面例子可以看出。如果一個個執行,那至少需要 1+32+64+128
/* 例3.10 */ function timerPromisefy(delay) { return new Promise(function (resolve) { setTimeout(function () { resolve(delay); }, delay); }); } var startDate = Date.now(); Promise.all([ timerPromisefy(1), timerPromisefy(32), timerPromisefy(64), timerPromisefy(128) ]).then(function (values) { console.log(Date.now() - startDate + 'ms'); console.log(values); }); -------output------- 133ms//不一定,但大於128ms [1,32,64,128]複製程式碼
.race()
語法:Promise.race(iterable) 複製程式碼
該方法同樣是將多個Promise例項,包裝成一個新的Promise例項。
var p = Promise.race([p1, p2, p3]);複製程式碼
Promise.race
方法同樣接受一個數組(或具有Iterator介面)作引數。當p1, p2, p3中有一個例項的狀態發生改變(變為 fulfilled
或 rejected
),p的狀態就跟著改變。並把第一個改變狀態的promise的返回值,傳給p的回撥函式。
/* 例3.11 */ var p1 = new Promise(function(resolve, reject) { setTimeout(reject, 500, "one"); }); var p2 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, "two"); }); Promise.race([p1, p2]).then(function(value) { console.log('resolve', value); }, function(error) { //not called console.log('reject', error); }); -------output------- resolve two var p3 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, "three"); }); var p4 = new Promise(function(resolve, reject) { setTimeout(reject, 100, "four"); }); Promise.race([p3, p4]).then(function(value) { //not called console.log('resolve', value); }, function(error) { console.log('reject', error); }); -------output------- reject four複製程式碼
在第一個promise物件變為resolve後,並不會取消其他promise物件的執行,如下例
/* 例3.12 */ var fastPromise = new Promise(function (resolve) { setTimeout(function () { console.log('fastPromise'); resolve('resolve fastPromise'); }, 100); }); var slowPromise = new Promise(function (resolve) { setTimeout(function () { console.log('slowPromise'); resolve('resolve slowPromise'); }, 1000); }); // 第一個promise變為resolve後程序停止 Promise.race([fastPromise, slowPromise]).then(function (value) { console.log(value);// => resolve fastPromise }); -------output------- fastPromise resolve fastPromise slowPromise//仍會執行復制程式碼
.resolve()
語法:
Promise.resolve(value); Promise.resolve(promise); Promise.resolve(thenable);複製程式碼
它可以看做 new Promise()
的快捷方式。
Promise.resolve('Success'); /*******等同於*******/ new Promise(function (resolve) { resolve('Success'); });複製程式碼
這段程式碼會讓這個Promise物件立即進入 resolved
狀態,並將結果 success
傳遞給 then
指定的 onFulfilled
回撥函式。由於 Promise.resolve()
也是返回Promise物件,因此可以用 .then()
處理其返回值。
/* 例3.13 */ Promise.resolve('success').then(function (value) { console.log(value); }); -------output------- Success複製程式碼
/* 例3.14 */ //Resolving an array Promise.resolve([1,2,3]).then(function(value) { console.log(value[0]);// => 1 }); //Resolving a Promise var p1 = Promise.resolve('this is p1'); var p2 = Promise.resolve(p1); p2.then(function (value) { console.log(value);// => this is p1 }); 複製程式碼
Promise.resolve()
的另一個作用就是將 thenable
物件(即帶有 then
方法的物件)轉換為promise物件。
/* 例3.15 */ var p1 = Promise.resolve({ then: function (resolve, reject) { resolve("this is an thenable object!"); } }); console.log(p1 instanceof Promise);// => true p1.then(function(value) { console.log(value);// => this is an thenable object! }, function(e) { //not called });複製程式碼
再看下面兩個例子,無論是在什麼時候拋異常,只要promise狀態變成 resolved
或 rejected
,狀態不會再改變,這和新建promise是一樣的。
/* 例3.16 */ //在回撥函式前拋異常 var p1 = { then: function(resolve) { throw new Error("error"); resolve("Resolved"); } }; var p2 = Promise.resolve(p1); p2.then(function(value) { //not called }, function(error) { console.log(error);// => Error: error }); //在回撥函式後拋異常 var p3 = { then: function(resolve) { resolve("Resolved"); throw new Error("error"); } }; var p4 = Promise.resolve(p3); p4.then(function(value) { console.log(value);// => Resolved }, function(error) { //not called });複製程式碼
.reject()
語法:Promise.reject(reason) 複製程式碼
這個方法和上述的 Promise.resolve()
類似,它也是 new Promise()
的快捷方式。
Promise.reject(new Error('error')); /*******等同於*******/ new Promise(function (resolve, reject) { reject(new Error('error')); });複製程式碼
這段程式碼會讓這個Promise物件立即進入 rejected
狀態,並將錯誤物件傳遞給 then
指定的 onRejected
回撥函式。
四 Promise常見問題
經過上一章的學習,相信大家已經學會使用 Promise
。
總結一下建立promise的流程:
- 使用
new Promise(fn)
或者它的快捷方式Promise.resolve()
、Promise.reject()
,返回一個promise物件 - 在
fn
中指定非同步的處理
處理結果正常,呼叫resolve
處理結果錯誤,呼叫reject
如果使用ES6的箭頭函式,將會使寫法更加簡單清晰。
這一章節,將會用例子的形式,以說明promise使用過程中的注意點及容易犯的錯誤。
情景1: reject 和 catch 的區別
- promise.then(onFulfilled, onRejected)
在onFulfilled
中發生異常的話,在onRejected
中是捕獲不到這個異常的。 - promise.then(onFulfilled).catch(onRejected)
.then
中產生的異常能在.catch
中捕獲
一般情況,還是建議使用第二種,因為能捕獲之前的所有異常。當然了,第二種的 .catch()
也可以使用 .then()
表示,它們本質上是沒有區別的, .catch === .then(null, onRejected)
情景2: 如果在then中拋錯,而沒有對錯誤進行處理(即catch),那麼會一直保持reject狀態,直到catch了錯誤
/* 例4.1 */ function taskA() { console.log(x); console.log("Task A"); } function taskB() { console.log("Task B"); } function onRejected(error) { console.log("Catch Error: A or B", error); } function finalTask() { console.log("Final Task"); } var promise = Promise.resolve(); promise .then(taskA) .then(taskB) .catch(onRejected) .then(finalTask); -------output------- Catch Error: A or B,ReferenceError: x is not defined Final Task複製程式碼
根據例4.1的輸出結果及流程圖,可以看出,A拋錯時,會按照 taskA → onRejected → finalTask這個流程來處理。A拋錯後,若沒有對它進行處理,如例3.7,狀態就會維持 rejected
,taskB不會執行,直到 catch
了錯誤。
/* 例4.2 */ function taskA() { console.log(x); console.log("Task A"); } function taskB() { console.log("Task B"); } function onRejectedA(error) { console.log("Catch Error: A", error); } function onRejectedB(error) { console.log("Catch Error: B", error); } function finalTask() { console.log("Final Task"); } var promise = Promise.resolve(); promise .then(taskA) .catch(onRejectedA) .then(taskB) .catch(onRejectedB) .then(finalTask); -------output------- Catch Error: A ReferenceError: x is not defined Task B Final Task複製程式碼
將例4.2與4.1對比,在taskA後多了對A的處理,因此,A拋錯時,會按照A會按照 taskA → onRejectedA → taskB → finalTask這個流程來處理,此時taskB是正常執行的。
情景3: 每次呼叫 then
都會返回一個新建立的promise物件,而 then
內部只是返回的資料
/* 例4.3 */ //方法1:對同一個promise物件同時呼叫 then 方法 var p1 = new Promise(function (resolve) { resolve(100); }); p1.then(function (value) { return value * 2; }); p1.then(function (value) { return value * 2; }); p1.then(function (value) { console.log("finally: " + value); }); -------output------- finally: 100 //方法2:對 then 進行 promise chain 方式進行呼叫 var p2 = new Promise(function (resolve) { resolve(100); }); p2.then(function (value) { return value * 2; }).then(function (value) { return value * 2; }).then(function (value) { console.log("finally: " + value); }); -------output------- finally: 400複製程式碼
第一種方法中, then
的呼叫幾乎是同時開始執行的,且傳給每個then的value都是100,這種方法應當避免。方法二才是正確的鏈式呼叫。
因此容易出現下面的錯誤寫法:
/* 例4.4 */ function badAsyncCall(data) { var promise = Promise.resolve(data); promise.then(function(value) { //do something return value + 1; }); return promise; } badAsyncCall(10).then(function(value) { console.log(value);//想要得到11,實際輸出10 }); -------output------- 10複製程式碼
正確的寫法應該是:
/* 改寫例4.4 */ function goodAsyncCall(data) { var promise = Promise.resolve(data); return promise.then(function(value) { //do something return value + 1; }); } goodAsyncCall(10).then(function(value) { console.log(value); }); -------output------- 11複製程式碼
情景4: 在非同步回撥中拋錯,不會被 catch
到
// Errors thrown inside asynchronous functions will act like uncaught errors var promise = new Promise(function(resolve, reject) { setTimeout(function() { throw 'Uncaught Exception!'; }, 1000); }); promise.catch(function(e) { console.log(e);//This is never called });複製程式碼
情景5: promise狀態變為 resove
或 reject
,就凝固了,不會再改變
console.log(1); new Promise(function (resolve, reject){ reject(); setTimeout(function (){ resolve();//not called }, 0); }).then(function(){ console.log(2); }, function(){ console.log(3); }); console.log(4); -------output------- 1 4 3複製程式碼
五 結語
關於 promise
就先介紹到這邊了,比較基礎,有不足的地方歡迎指出,有更好的也歡迎補充~
參考資料:
轉載自:https://segmentfault.com/a/1190000007032448