在前端開發中,有個很熟悉的詞叫做“回撥”,在處理一些非同步的函式的時候,回撥被廣泛應用,但是大量用回撥來程式設計,會出現巢狀層級過多,程式碼風格不規範,不清晰的問題。“Promise/A+規範”是一種很方便的非同步程式設計方式。
使用promise進行程式設計有哪些好處?
- 將複雜的非同步處理輕鬆地進行模式化
- 程式碼更清晰
- 異常處理更方便
- 程式碼鏈式操作,爽!
先來一段簡單的promise的程式碼:
var promise = getAsyncPromise("fileA.txt");
promise.then(function(result){
// 獲取檔案內容成功時的處理
}).catch(function(error){
// 獲取檔案內容失敗時的處理
});
複製程式碼
通過這個程式碼,是不是覺得promise非同步程式設計很清晰?
下面我詳細介紹下promise實用的方法。
Promise 簡介
1. 構建promise物件
- Promise類似於 XMLHttpRequest ,從建構函式 Promise 來建立一個新建新 promise 對 象作為介面。
- 要想建立一個promise物件、可以使用 new 來呼叫 Promise 的構造器來進行例項化。如下:
var promise = new Promise(function(resolve, reject) {
// 非同步處理
// 處理結束後、呼叫resolve 或 reject
});
複製程式碼
2. 常用的方法
- 對通過new生成的promise物件為了設定其值在 resolve(成功) / reject(失敗)時呼叫的回撥函式 可以使用 promise.then() 例項方法。
promise.then(onFulfilled, onRejected)
//resolve(成功)時 onFulfilled 會被呼叫
//reject(失敗)時onRejected 會被呼叫
//onFulfilled 、 onRejected 兩個都為可選引數。
複製程式碼
- promise.then成功和失敗時都可以使用。另外在只想對異常進行處理時可以採用promise.then(undefined, onRejected),這種方式,只指定reject時的回撥函式即可。 不過這種情況下 promise.catch(onRejected) 應該是個更好的選擇。
promise.catch(onRejected)
複製程式碼
3. 靜態方法
- 包括 Promise.all() 還有 Promise.resolve() 等在內,主要都是一些對Promise進行操作的 輔助方法。
4. 來個栗子
function asyncFunction() {
return new Promise(function (resolve, reject) { //①
setTimeout(function () {
resolve('Async Hello world'); }, 16); //②
});
}
asyncFunction().then(function (value) { console.log(value); // => 'Async Hello world'
}).catch(function (error) { console.log(error);
});
複製程式碼
栗子說明:
- new Promise構造器之後,會返回一個promise物件
- 為promise物件用設定 .then 呼叫返回值時的回撥函式。
- 這個函式會返回promise物件,對於這個promise物件,我們呼叫的 方法來設定resolve後的回撥函式,catch方法來設定發生錯誤時的回撥函式。
- 在這種情況下catch的回撥函式並不會被執行(因為promise返回了resolve), 不過如果執行環境沒有提供setTimeout函式的話,那麼上面程式碼在執行中就會產生異常,在catch中設定的回撥函式就會被執行。
上面的程式碼也也可以不用catch方法,用then(resolve, reject)的形式(鏈式操作時最好用catch,可以統一捕捉異常):
asyncFunction().then(function (value) { console.log(value);
}, function (error) { console.log(error);
});
複製程式碼
Promise 狀態詳解
大致的瞭解promise處理流程,單獨介紹一下promise狀態
1. 大致三個狀態
- resolve(成功)時。此時會呼叫 onFulfilled
- reject(失敗)時。此時會呼叫 onRejected
- 既不是resolve也不是reject的狀態。也就是promise物件剛被建立後的初始化狀態等
*注意:從Pending轉換為Fulfilled或Rejected之後, 這個promise物件的狀 態就不會再發生任何變化。
當promise的物件狀態發生變化時,用 .then 來定義只會被呼叫一次的函式。
編寫Promise程式碼:
1. 建立promise物件
- new Promise(fn) 返回一個promise物件
- 在 fn 中指定非同步等處理 • 處理結果正常的話,呼叫 resolve(處理結果值) • 處理結果錯誤的話,呼叫 reject(Error物件)
來個XHR的promise物件的栗子
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest(); //栗子就不做ie相容了
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
}
else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}
// 執行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value) {
console.log(value); }).catch(function onRejected(error) {
console.error(error); });
複製程式碼
栗子說明:
- getURL 只有在通過XHR取得結果狀態為200時才會呼叫 resolve - 也就是隻有資料取 得成功時,而其他情況(取得失敗)時則會呼叫 reject 方法。
- resolve(req.responseText) 在response的內容中加入了引數。 resolve方法的引數並沒有特 別的規則,基本上把要傳給回撥函式引數放進去就可以了。 ( then 方法可以接收到這 個引數值)
6. 編寫promise物件處理方法
讓我們在實際中使用一下剛才建立的返回promise物件的函式
getURL("http://example.com/"); // => 返回promise物件
複製程式碼
為promise物件新增處理方法主要有以下兩種:
- promise物件被 resolve 時的處理(onFulfilled)
- promise物件被 reject時的處理(onRejected)
getURL(URL).then(onFulfilled, onRejected); //純then寫法
getURL(URL).then( onFulfilled).catch( onRejected); // then chatch寫法
複製程式碼
注:一般說來,使用 .catch 來將resolve和reject處理分開來寫是比較推薦的做法, 這兩者的 區別會在then和catch的區別中再做詳細介紹。
詳解promise
1. 使用 Promise.resolve
- Promise.resolve(value) 可以認為是 new Promise() 方法的快捷方式。
//比如 Promise.resolve(42); 可以認為是以下程式碼的語法糖。
new Promise(function(resolve){
resolve(42);
});
複製程式碼
在這段程式碼中的 resolve(42);會讓這個promise物件立即進入確定(即resolved)狀態,並將 42 傳遞給後面then裡所指定的 onFulfilled 函式。
Promise.resolve(42).then(
function (value) {
console.log(value) //42
}
);
複製程式碼
注:Promise.resolve作為newPromise()的快捷方式,在進行promise物件的初始化或者編寫 測試程式碼的時候都非常方便。
-
Promise.resolve方法另一個作用就是將thenable物件轉換為promise物件。
**thenable物件**:簡單來說它就是一個非常類似promise的東西(*就像我們有時稱具有.length方法的非陣列物件為Arraylike一樣,thenable指的是一個具有.then 方法的物件*。) 複製程式碼
-
最簡單的例子就是jQuery.ajax(),它的返回值就是thenable的(因為 jQuery.ajax()的返回值是jqXHRObject物件,這個物件具有 .then 方法)
-
用Promise.resolve來轉換為一個promise物件。變成了promse物件的話,就能直接使用 then 或者 catch等這些在ES6Promises裡定 義的方法了。
-
var promise = Promise.resolve(
$.ajax('/json/comment.json')
) // => promise物件
promise.then(function(value){
console.log(value);
});
複製程式碼
總結:簡單總結一下 Promise.resolve 方法的話,可以認為它的作用就是將傳遞給它的引數填 充(Fulfilled)到promise物件後並返回這個promise物件。
2. 使用 Promise.reject
- Promise.reject(error) 是和 Promise.resolve(value) 類似的靜態方法,是 new Promise() 方 法的快捷方式。
new Promise(
function (resolve,reject) {
reject(new Error("出錯了"));
}
);
// 是Promise.reject(newError("出錯了"))的語法糖。
//如下:
Promise.reject(new Error("BOOM!")).catch(
function(error){
console.error(error);
}
);
複製程式碼
上一篇寫了一部分,再寫一些關於promise的筆記
Promise.then() &&鏈式寫法
- promise可以寫成方法鏈的形式
aPromise.then(function taskA(value) {
// task A
}).then(function taskB(vaue){
// task B
}).catch(function onRejected(error){ //最後catch 統一捕捉異常
console.log(error);
});
複製程式碼
來段程式碼,看下鏈式操作的執行流程
function taskA() {
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);
複製程式碼
上面這段程式碼的執行流程如下圖:
註釋: .cntch() 可以捕捉 taskA taskB出現的異常,但是 onRejected、finalTask這兩個後面沒有catch捕捉了,所以這兩個函式出現問題不會被捕捉到。
- 用then、catch 鏈式寫法,要比純then寫法好!!
- catch 可以捕捉到resolve函式裡面的錯誤,有時候我可以主動在resolve函式裡throw一個錯誤或者返回一個Rejected狀態的promise物件(推薦後者);
- 鏈式操作程式碼更簡潔
Promise鏈式寫法中引數如何傳遞
前面例子中的Task都是相互獨立的,只是被簡單呼叫而已。這時候如果 Task A 想給 Task B 傳遞一個引數,那就是在 Task A 中 return 的返回值,會在 Task B 執行時傳給它。
function doubleUp(value) {
return value * 2;
}
function increment(value) {
return value + 1;
}
function output(value) {
console.log(value);
}
var promise = Promise.resolve(1);
promise
.then(increment)
.then(doubleUp)
.then(output)
.catch(function(error){
// promise chain中出現異常的時候會被呼叫
console.error(error);
});
1. Promise.resolve(1); 傳遞 1 給 increment 函式
2. 函式 increment 對接收的引數進行 +1 操作並返回(通過 return ) 3. 這時引數變為2,並再次傳給 doubleUp 函式
3. 最後在函式 output 中列印結果
複製程式碼
**注:**每個方法中return的值不僅只侷限於字串或者數值型別,也可以是物件或者promise 物件等複雜型別。
- return的值會由Promise.resolve進行相應的包裝處理,因此不回撥函式中會返回一個什麼樣的值,最終的結果都是返回一個新建立的promise物件。
Promise.catch 的一些小問題
- IE8 相容問題catch是ECMAScrip3中保留字是不能作為物件的屬性名使用的(ie8大概是基於es3實現的)可以用Promise['catch']來呼叫(不過很多壓縮、打包工具自帶轉換功能,還不錯)
Promise處理多個非同步任務,promise.all 上場
Promise.all 接收一個promise物件的陣列作為引數,當這個陣列裡的所有promise物件 全部變為resolve或reject狀態的時候,它才會去呼叫 .then 方法。
// `delay`毫秒後執行resolve
function timerPromisefy(delay) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(delay); }, delay
);
});
}
var startDate = Date.now();
// 所有promise變為resolve後程式退出
Promise.all([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
]).then(function (values) {
console.log(Date.now() - startDate + 'ms'); // 約128ms
console.log(values); // [1,32,64,128]
}
);
複製程式碼
**注:**從總用時120ms來看,傳遞給Promise.all的promise並不是一個個的順序執行的,而是 同時開始、並行執行的。
Promise處理多個非同步任務,Promise.race 上場
它的使用方法和Promise.all一樣,接收一個promise物件陣列為引數
Promise.all 在接收到的所有的物件promise都變為 FulFilled 或者 Rejected 狀態之後才會繼續進行後面的處理,與之相對的是Promise.race 只要有一個promise物件進入FulFilled或者Rejected狀態的話,就會繼續進行後面的處理。
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
]).then(function (value) {
console.log(value); // => 1 只列印出一個最先出結果的
});
複製程式碼