js promise詳解
https://www.jb51.net/article/139825.htm
1、約定
- 本文的 demo 程式碼有些是虛擬碼,不可以直接執行。
- 沒有特殊說明,本文所有 demo 都是基於 ES6 規範。
- Object.method 代表是靜態方法, Object#method 代表的是例項方法。如 Promise#then 代表的是 Promise 的例項方法, Promise.resolve 代表的是 Promise 的靜態方法.
2、什麼是 Promise?
首先我們來了解 Promise 到底是怎麼一回事
Promise 是抽象的非同步處理物件,以及對其進行各種操作的元件。我知道這樣解釋你肯定還是不明白 Promise 是什麼東西,你可以把 Promise 理解成一個 容器,裡面裝著將來才會結束的一個事件的結果,這個事件通常是一個非同步操作。
Promise最初被提出是在 E語言中, 它是基於並列/並行處理設計的一種程式語言。Javascript 在 ES6 之後也開始支援 Promise 特性了,用來解決非同步操 的問題。這裡順便解釋一下什麼是 ES6, ECMAScript 是 Javascript 語言的國際標準,Javascript 是 ECMAScript 的有一個實現, 而ES6(全稱 ECMAScript 6)是這個標準的一個版本。
3、Javascript 為什麼要引入 Promise?
細心的你可能發現了我剛剛說了 Javascript 支援 Promise 實現是為了解決非同步操作的問題。談到非同步操作,你可能會說,Javascript 不是可以用回撥 函式處理非同步操作嗎? 原因就是 Promise 是一種更強大的非同步處理方式,而且她有統一的 API 和規範,下面分別看看傳統處理非同步操作和 Promise 處理 非同步操作有哪些不同。
使用回撥函式處理非同步操作:
1 2 3 4 5 6 7 |
login( "http://www.r9it.com/login.php" , function (error, result){
// 登入失敗處理
if (error){
throw error;
}
// 登入成功時處理
});
|
Node.js等則規定在JavaScript的回撥函式的第一個引數為 Error 物件,這也是它的一個慣例。 像上面這樣基於回撥函式的非同步處理如果統一引數使用規則的話,寫法也會很明瞭。 但是,這也僅是編碼規約而已,即使採用不同的寫法也不會出錯。 而Promise則是把類似的非同步處理物件和處理規則進行規範化, 並按照採用統一的介面來編寫,而採取規定方法之外的寫法都會出錯。
使用 Promise 處理非同步操作:
1 2 3 4 5 6 |
var promise = loginByPromise( "http://www.r9it.com/login.php" );
promise.then( function (result){
// 登入成功時處理
}). catch ( function (error){
// 登入失敗時處理
});
|
通過上面兩個 demo 你會發現,有了Promise物件,就可以將非同步操作以同步操作的流程表達出來。 這樣在處理多個非同步操作的時候還可以避免了層層巢狀的回撥函式(後面會有演示)。 此外,Promise物件提供統一的介面,必須通過呼叫Promise#then
和Promise#catch
這兩個方法來結果,除此之外其他的方法都是不可用的,這樣使得非同步處理操作更加容易。
4、基本用法
在 ES6 中,可以使用三種辦法建立 Promise 例項(物件)
(1). 構造方法
1 2 3 |
let promies = new Promise((resolve, reject) => {
resolve(); //非同步處理
});
|
Promise 建構函式接受一個函式作為引數,該函式的兩個引數分別是 resolve 和 reject。它們是兩個函式,由 JavaScript 引擎提供,不用自己部署。
(2). 通過 Promise 例項的方法,Promise#then 方法返回的也是一個 Promise 物件
1 |
promise.then(onFulfilled, onRejected);
|
(3). 通過 Promise 的靜態方法,Promise.resolve(),Promise.reject()
1 2 3 4 |
var p = Promise.resolve();
p.then( function (value) {
console.log(value);
});
|
4.1 Promise 的執行流程
- new Promise構造器之後,會返回一個promise物件;
- 為 promise 註冊一個事件處理結果的回撥函式(resolved)和一個異常處理函式(rejected);
4.2 Promise 的狀態
例項化的 Promise 有三個狀態:
Fulfilled: has-resolved, 表示成功解決,這時會呼叫 onFulfilled.
Rejected: has-rejected, 表示解決失敗,此時會呼叫 onRejected.
Pending: unresolve, 表示待解決,既不是resolve也不是reject的狀態。也就是promise物件剛被建立後的初始化狀態.
上面我們提到 Promise 建構函式接受一個函式作為引數,該函式的兩個引數分別是 resolve 和 reject.
resolve函式的作用是,將 Promise 物件的狀態從 未處理 變成 處理成功 (unresolved => resolved), 在非同步操作成功時呼叫,並將非同步操作的結果作為引數傳遞出去。
reject函式的作用是,將 Promise 物件的狀態從 未處理 變成 處理失敗 (unresolved => rejected), 在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
Promise 例項生成以後,可以用 then 方法和 catch 方法分別指定 resolved 狀態和 rejected 狀態的回撥函式。
以下是 Promise 的狀態圖
4.3 Promise 的基本特性
【1】 物件的狀態不受外界影響 Promise 物件代表一個非同步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。 只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。 這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法變。
【2】 一旦狀態改變,就不會再變,任何時候都可以得到這個結果 Promise物件的狀態改變,只有兩種可能:從 pending 變為 fulfilled 和從 pending 變為 rejected。 只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。 如果改變已經發生了,你再對 Promise 物件添加回調函式,也會立即得到這個結果。 這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
例如以下程式碼, reject 方法是無效的
1 2 3 4 5 6 7 8 9 |
var promise = new Promise((fuck, reject) => {
resolve( "xxxxx" );
//下面這行程式碼無效,因為前面 resolve 方法已經將 Promise 的狀態改為 resolved 了
reject( new Error());
});
promise.then((value) => {
console.log(value);
})
|
下圖是 Promise 的狀態處理流程圖
5、Promise 的執行順序
我們知道 Promise 在建立的時候是立即執行的,但是事實證明 Promise 只能執行非同步操作,即使在建立 Promise 的時候就立即改變它狀態。
1 2 3 4 5 6 7 8 9 10 |
var p = new Promise((resolve, reject) => {
console.log( "start Promise" );
resolve( "resolved" );
});
p.then((value) => {
console.log(value);
})
console.log( "end Promise" );
|
列印的結果是:
start Promise
end Promise
resolved
或許你會問,這個操作明明是同步的,定義 Promise 裡面的程式碼都被立即執行了,那麼回撥應該緊接著 resolve 函式執行,那麼應該先列印 “resolved” 而不應該先列印 “end Promise”.
這個是 Promise 規範規定的,為了防止同步呼叫和非同步呼叫同時存在導致的混亂
6、Promise 的鏈式呼叫(連貫操作)
前面我們講過,Promise 的 then 方法以及 catch 方法返回的都是新的 Promise 物件,這樣我們可以非常方便的解決巢狀的回撥函式的問題, 也可以很方便的實現流程任務。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var p = new Promise( function (resolve, reject) {
resolve();
});
function taskA() {
console.log( "Task A" );
}
function taskB() {
console.log( "Task B" );
}
function taskC() {
console.log( "Task C" );
}
p.then(taskA())
.then(taskB())
.then(taskC())
. catch ( function (error) {
console.log(error);
});
|
上面這段程式碼很方便的實現了從 taskA 到 taskC 的有序執行。
當然你可以把 taskA - taskC 換成任何非同步操作,如從後臺獲取資料:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var getJSON = function (url, param) {
var promise = new Promise( function (resolve, reject){
var request = require( 'ajax-request' );
request({url:url, data: param}, function (err, res, body) {
if (!err && res.statusCode == 200) {
resolve(body);
} else {
reject( new Error(err));
}
});
});
return promise;
};
var url = "login.php" ;
getJSON(url, {id:1}).then(result => {
console.log(result);
return getJSON(url, {id:2})
}).then(result => {
console.log(result);
return getJSON(url, {id:3});
}).then(result => {
console.log(result);
}). catch (error => console.log(error));
|
這樣用起來似乎很爽,但是有個問題需要注意,我們說過每個 then() 方法都返回一個新的 Promise 物件,那既然是 Promise 物件,那肯定就有註冊 onFulfilled 和 onRejected, 如果某個任務流程的 then() 方法鏈過長的話,前面的任務丟擲異常,會導致後面的任務被跳過。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function taskA() {
console.log( "Task A" );
throw new Error( "throw Error @ Task A" );
}
function taskB() {
console.log( "Task B" );
}
function onRejected(error) {
console.log(error);
}
function finalTask() {
console.log( "Final Task" );
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
. catch (onRejected)
.then(finalTask);
|
執行的結果是:
Task A
Error: throw Error @ Task A
Final Task
顯然, 由於 A 任務丟擲異常(執行失敗),導致 .then(taskB) 被跳過,直接進入 .catch 異常處理環節。
6.1 promise chain 中如何傳遞引數
上面我們簡單闡述了 Promise 的鏈式呼叫,能夠非常有效的處理非同步的流程任務。
但是在實際的使用場景中,任務之間通常都是有關聯的,比如 taskB 需要依賴 taskA 的處理結果來執行,這有點類似 Linux 管道機制。 Promise 中處理這個問題也很簡單,那就是在 taskA 中 return 的返回值,會在 taskB 執行時傳給它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function taskA() {
console.log( "Task A" );
return "From Task A" ;
}
function taskB(value) {
console.log(value);
console.log( "Task B" );
return "From Task B" ;
}
function onRejected(error) {
console.log(error);
}
function finalTask(value) {
console.log(value);
console.log( "Final Task" );
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
. catch (onRejected)
.then(finalTask);
|
搞定,就這麼簡單!
6.2 resolve 和 reject 引數
reject函式的引數通常是Error物件的例項,表示丟擲的錯誤;resolve函式的引數除了正常的值以外,還可能是另一個 Promise 例項, 比如像上面的 getJSON() 方法一樣。
1 2 3 4 5 6 7 8 9 10 11 |
var p1 = new Promise( function (resolve, reject) {
setTimeout(() => reject( new Error( 'fail' )), 3000)
})
var p2 = new Promise( function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
. catch (error => console.log(error))
|
注意,這時p1的狀態就會傳遞給p2,也就是說,p1的狀態決定了p2的狀態。
如果p1的狀態是 pending,那麼p2的回撥函式就會等待p1的狀態改變;
如果p1的狀態已經是 resolved 或者 rejected,那麼p2的回撥函式將會立刻執行。
7、Promise 基本方法
ES6的Promise API提供的方法不是很多,下面介紹一下 Promise 物件常用的幾個方法.
7.1 Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回撥函式。
1 2 3 4 5 6 |
p.then((val) => console.log( 'fulfilled:' , val))
. catch ((err) => console.log( 'rejected' , err));
// 等同於
p.then((val) => console.log( 'fulfilled:' , val))
.then( null , (err) => console.log( "rejected:" , err));
|
Promise 物件的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。 所以通常建議使用 catch 方法去捕獲異常,而不要用 then(null, function(error) {}) 的方式,因為這樣只能捕獲當前 Promise 的異常
1 2 3 4 5 6 7 |
p.then(result => {console.log(result)})
.then(result => {console.log(result)})
.then(result => {console.log(result)})
. catch (error => {
//捕獲上面三個 Promise 物件產生的異常
console.log(error);
});
|
跟傳統的try/catch程式碼塊不同的是,如果沒有使用catch方法指定錯誤處理的回撥函式,Promise 物件丟擲的錯誤不會傳遞到外層程式碼,即不會有任何反應。
通俗的說法就是“Promise 會吃掉錯誤”。
比如下面的程式碼就出現這種情況
1 2 3 4 5 6 7 |
var p = new Promise( function (resolve, reject) {
// 下面一行會報錯,因為x沒有宣告
resolve(x + 2);
});
p.then(() => {console.log( "every thing is ok." );});
// 這行程式碼會正常執行,不會受 Promise 裡面報錯的影響
console.log( "Ok, it's Great." );
|
7.2 Promise.all()
Promise.all方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。用來處理組合 Promise 的邏輯操作。
1 |
var p = Promise.all([p1, p2, p3]);
|
上面程式碼 p 的狀態由p1、p2、p3決定,分成兩種情況。
- 只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回撥函式。
- 只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。
下面是一個具體的例子
1 2 3 4 5 6 7 8 9 10 |
// 生成一個Promise物件的陣列
var promises = [1,2,3,4,5,6].map( function (id) {
return getJSON( '/post/' + id + ".json" );
});
Promise.all(promises).then( function (posts) {
// ...
}). catch ( function (reason){
// ...
});
|
上面程式碼中,promises 是包含6個 Promise 例項的陣列,只有這6個例項的狀態都變成 fulfilled,或者其中有一個變為 rejected, 才會呼叫 Promise.all 方法後面的回撥函式。
7.3 Promise.race()
Promise.race方法同樣是將多個Promise例項,包裝成一個新的Promise例項。 與 Promise.all 不同的是,只要有一個 promise 物件進入 FulFilled 或者 Rejected 狀態的話,Promise.rece 就會繼續進行後面的處理
1 |
var p = Promise.race([p1, p2, p3]);
|
上面程式碼中,只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。 Promise.race 方法的引數與 Promise.all 方法一樣,如果不是 Promise 例項,就會先呼叫 Promise.resolve 方法, 將引數轉為 Promise 例項,再進一步處理。
下面是一個具體的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// `delay`毫秒後執行resolve
function timerPromisefy(delay) {
return new Promise( function (resolve) {
setTimeout( function () {
resolve(delay);
}, delay);
});
}
// 任何一個promise變為resolve或reject 的話程式就停止執行
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
]).then( function (value) {
console.log(value); // => 1
});
|
7.4 Promise.resolve()
Promise.resolve 方法有2個作用,一個就是前面我們說的,它是通過靜態方法建立 Promise 例項的渠道之一, 另一個作用就是將 Thenable 物件轉換為 Promise 物件。
那麼什麼是 Thenable 物件呢?ES6 Promises裡提到了Thenable這個概念,簡單來說它就是一個非常類似promise的東西。 就像我們有時稱具有 .length 方法的非陣列物件為 Array like 一樣,Thenable 指的是一個具有 .then 方法的物件。
這種將 Thenable物件轉換為 Promise 物件的機制要求thenable物件所擁有的 then 方法應該和Promise所擁有的 then 方法具有同樣的功能和處理過程, 在將 Thenable 物件轉換為 Promise 物件的時候,還會巧妙的利用 Thenable 物件原來具有的 then 方法。
到底什麼樣的物件能算是 Thenable 的呢,最簡單的例子就是 jQuery.ajax(),它的返回值就是 Thenable 的。
將 Thenable 物件轉換為 Promise 物件
1 2 3 4 |
var promise = Promise.resolve($.ajax( '/json/comment.json' )); // => promise物件
promise.then( function (value){
console.log(value);
});
|
除了上面的方法之外,Promise.resolve方法的引數還有以下三種情況。
(1). 引數是一個 Promise 例項
如果引數是Promise例項,那麼Promise.resolve將不做任何修改、原封不動地返回這個例項。
(2). 引數不是具有then方法的物件,或根本就不是物件
如果引數是一個原始值,或者是一個不具有then方法的物件,則Promise.resolve方法返回一個新的Promise物件,狀態為resolved。
1 2 3 4 |
var p = Promise.resolve( 'Hello' );
p.then( function (s){
console.log(s)
});
|
上面程式碼生成一個新的Promise物件的例項p。由於字串Hello不屬於非同步操作(判斷方法是字串物件不具有then方法), 返回Promise例項的狀態從一生成就是resolved,所以回撥函式會立即執行。 Promise.resolve方法的引數,會同時傳給回撥函式。
(3). 不帶有任何引數
Promise.resolve方法允許呼叫時不帶引數,直接返回一個resolved狀態的Promise物件。這個我們在上面講建立 Promise 例項的三種方法的時候就講過了
1 2 3 4 |
var p = Promise.resolve();
p.then( function () {
// ...
});
|
7.5 Promise.reject()
Promise.reject(reason)方法也會返回一個新的 Promise 例項,該例項的狀態為rejected。 需要注意的是,Promise.reject()方法的引數,會原封不動地作為 reject 的理由,變成後續方法的引數。這一點與 Promise.resolve 方法不一致。
1 2 3 4 5 6 7 8 9 10 11 |
const thenable = {
then(resolve, reject) {
reject( '出錯了' );
}
};
Promise.reject(thenable)
. catch (e => {
console.log(e === thenable)
})
// true
|
上面程式碼中,Promise.reject 方法的引數是一個 thenable 物件,執行以後,後面 catch 方法的引數不是 reject 丟擲的“出錯了”這個字串, 而是 thenable 物件。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援指令碼之家。