promise是什麼?
Promise是異步編程的一種解決方案,比傳統的回調函數和事件更合理和強大。
所謂Promise,簡單來說就是一個容器,裏面保存着某個未來纔會結束的事情(通常是一個異步操作)。從語法上說,Promise是一個對象,從他可以獲取異步操作的消息。
特點:
-
對象的狀態不受外界影響。Promise對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(以失敗)。只有異步操作的結果可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來。
-
一旦狀態改變,就不會再變,任何時候都是可以得到這個結果的。Promise對象的狀態改變只有兩種可能:*從pending變爲fulfilled和從pending變爲rejected。只要這兩種情況發生,狀態就會凝固,不會再變了。再對Promise對象添加回調函數也會立即得到這個結果。
有了Promise對象,就可以將異步操作以同步操作的流程表達出來。
缺點:
首先無法取消Promise,一旦新建他就會立即執行,無法中途取消。其次,如果不設置回調函數,Promise內部跑出的錯誤無法反應到外部。當pending的時候,無法知道進展到了哪一步。
基本用法
ES6規定,Promise對象是一個構造函數,用來生成Promise實例。
下面代碼創造了一個Promise實例。
const promise = new Promise(function(resolve, reject) {
if(success) {
resolve(value)
} else {
reject(error)
}
})
Promise構造函數接受一個函數作爲參數,該函數的兩個參數分別是resolve和reject。它們是兩個函數,由JavaScript引擎提供,不用自己部署。
resolve函數的作用是,將Promise對象的狀態從"未完成"變成"成功"。(即從pending變爲resolved)。在異步操作成功的時候調用,並將異步操作結果作爲參數傳遞出去;
reject函數的作用是,將promise對象的狀態從"未完成"變成"失敗"(即從pending變爲rejected)。在異步操作失敗時調用,並將異步操作報出的錯誤,作爲參數傳遞出去。
Promise實例生成後,可以用then方法分別指定resolve狀態和rejected狀態的回調函數。
promise.then(function(value) {
}, function(error) {
})
then方法可以接受兩個回調函數作爲參數,
第一個回調函數是promise對象的狀態變爲resolved的時候調用,
第二個回調函數是promise對象的狀態變爲rejected時調用。
其中第二個函數是可選的,不一定需要提供。
這兩個函數都接受Promise對象傳出的值作爲參數。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done')//setTimeout 傳參
})
}
timeout(100).then((value) => {
console.log(value)//done
})
上面代碼中,timeout方法返回一個Promise實例,表示一段時間後纔會發生的結果。
過了指定的時間以後,Promise實例的狀態變爲resolved,就會觸發then方法綁定的回調函數。
Promise新建後就會立即執行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise')
resolve()
})
promise.then(function() {
console.log('resolved')
})
console.log('Hi')
//Promise
//Hi
//resolved
上面代碼中,Promise新建後立即執行,所以首先輸出的是Promise,然後then方法指定回調函數,將在當前腳本所有同步任務執行完成後纔會執行,所以resolved最後輸出。
如下是一個異步加載圖片的例子:
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image()
image.onload = function() {
resolve(image)
}
image.onerror = function() {
reject(new Error('count not load...'))
}
image.src = url
})
}
上面代碼中,使用Promise包裝一個圖片加載的異步操作,如果加載成功就調用resolve方法, 否則就調用rejected方法。
1. Promise.prototype.then()
Promise實例具有then方法,也就是說then方法時定義在原型對象上的。
它的作用是爲Promise實例添加狀態改變時的回調函數。
前面說過,then方法的第一個參數是resolved狀態的回調函數,第二個參數是rejected狀態的回調函數(可選)。
then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)因此可以採用鏈式寫法,即then方法後面再調用另一個then方法。
採用鏈式的then可以指定一組按照次序調用的回調函數。這時,前一個回調函數可能返回一個還是Promise對象(即有異步操作),這時候一個回調函數就會等該Promise對象的狀態發生變化,纔會被調用
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL)
}).then(function funcA() {
console.log("resolved:", comments)
}, function funcB(err) {
console.log("rejected:", err)
})
上面代碼中,第一個then方法指定的回調函數,返回的是一個Promise對象。這時,第二個方法指定的回調函數,就會等待這個新的Promise對象狀態發生變化。如果變爲resolved,就調用funcA, 如果狀態變爲rejected,就調用funcB.
2. Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回調函數
getJSON('/post/1.json').then(function(posts) {
//...
}).catch(function() {
console.log('發生錯誤', error)
})
上面代碼中,getJSON方法返回一個Promise對象,如果該對象狀態變爲resolved,則會調用then方法指定的回調函數;如果異步操作拋出錯誤,狀態就會變爲rejected,就會調用catch方法指定的回調函數。另外,then方法指定的回調函數,如果運行拋出錯誤,也會被catch方法捕獲。
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狀態以及變成resolved,再拋出錯誤是無效的。因爲Promise的狀態一旦改變,就永久保持該狀態,不會再變了。
Promise對象的錯誤具有冒泡性質,會一直向後傳遞,直到被捕獲爲止,也就是說錯誤總會被下一個catch語句捕獲。
3. Promise.prototype.finally()
finally方法用於指定不管Promise對象最後狀態如何,都會執行的操作。
4.Pomise.all的使用
Promise.all(iterable) 方法返回一個 Promise 實例,此實例在 iterable 參數內所有的 promise 都“完成(resolved)”或參數中不包含 promise 時回調完成(resolve);如果參數中 promise 有一個失敗(rejected),此實例回調失敗(reject),失敗原因的是第一個失敗 promise 的結果。
具體代碼如下:
let t1 = new Promise((resolve,reject)=>{
resolve("t1-success")
})
let t2 = new Promise((resolve,reject)=>{
resolve("t2-success")
})
let t3 =Promise.reject("t3-error");
Promise.all([t1,t2,t3]).then(res=>{
console.log(res)
}).catch(error=>{
console.log(error)
})
//打印出來是t3-error
Promse.all在處理多個異步處理時非常有用,比如說一個頁面上需要等兩個或多個ajax的數據回來以後才正常顯示,在此之前只顯示loading圖標。
let request = (time,id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`第${id}個請求${time / 1000}秒`)
}, time)
})
}
let p1 = request(3000,1)
let p2 = request(2000,2)
Promise.all([p1, p2]).then((result) => {
console.log(result) // [ '第1個請求3秒', '第2個請求2秒' ]
}).catch((error) => {
console.log(error)
})
需要特別注意的是,Promise.all獲得的成功結果的數組裏面的數據順序和Promise.all接收到的數組順序是一致的,即p1的結果在前,即便p1的結果獲取的比p2要晚。這帶來了一個絕大的好處:在前端開發請求數據的過程中,偶爾會遇到發送多個請求並根據請求順序獲取和使用數據的場景,使用Promise.all毫無疑問可以解決這個問題。
5、Promise.race的使用
Promise.race(iterable) 方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。
顧名思義,Promse.race就是賽跑的意思,意思就是說,Promise.race([p1, p2, p3])裏面哪個結果獲得的快,就返回那個結果,不管結果本身是成功狀態還是失敗狀態。
let f1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
},1000)
})
let f2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed')
}, 500)
})
Promise.race([f1, f2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 打開的是 'failed'
})
原理是挺簡單的,但是在實際運用中還沒有想到什麼的使用場景會使用到。
舉例:超時取消
我們來看一下如何使用Promise.race來實現超時機制。
當然XHR有一個 timeout 屬性,使用該屬性也可以簡單實現超時功能,但是爲了能支持多個XHR同時超時或者其他功能,我們採用了容易理解的異步方式在XHR中通過超時來實現取消正在進行中的操作。
1. 讓Promise等待指定時間
首先我們來看一下如何在Promise中實現超時。
所謂超時就是要在經過一定時間後進行某些操作,使用 setTimeout 的話很好理解。
首先我們來串講一個單純的在Promise中調用 setTimeout 的函數。
//delayPromise.js
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
delayPromise(ms) 返回一個在經過了參數指定的毫秒數後進行onFulfilled操作的promise對象,這和直接使用 setTimeout 函數比較起來只是編碼上略有不同,如下所示。
setTimeout(function () {
alert("已經過了100ms!");
}, 100);
// == 幾乎同樣的操作
delayPromise(100).then(function () {
alert("已經過了100ms!");
});
在這裏 promise對象 這個概念非常重要,請切記。
2. Promise.race中的超時
我們可以將剛纔的 delayPromise 和其它promise對象一起放到 Promise.race 中來是實現簡單的超時機制。
//simple-timeout-promise.js
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
function timeoutPromise(promise, ms) {
var timeout = delayPromise(ms).then(function () {
throw new Error('Operation timed out after ' + ms + ' ms');
});
return Promise.race([promise, timeout]);
}
函數 timeoutPromise(比較對象promise, ms) 接收兩個參數,第一個是需要使用超時機制的promise對象,第二個參數是超時時間,它返回一個由 Promise.race 創建的相互競爭的promise對象。
之後我們就可以使用 timeoutPromise 編寫下面這樣的具有超時機制的代碼了。
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
function timeoutPromise(promise, ms) {
var timeout = delayPromise(ms).then(function () {
throw new Error('Operation timed out after ' + ms + ' ms');
});
return Promise.race([promise, timeout]);
}
// 運行示例
var taskPromise = new Promise(function(resolve){
// 隨便一些什麼處理
var delay = Math.random() * 2000;
setTimeout(function(){
resolve(delay + "ms");
}, delay);
});
timeoutPromise(taskPromise, 1000).then(function(value){
console.log("taskPromise在規定時間內結束 : " + value);
}).catch(function(error){
console.log("發生超時", error);
});
雖然在發生超時的時候拋出了異常,但是這樣的話我們就不能區分這個異常到底是_普通的錯誤_還是_超時錯誤_了。
爲了能區分這個 Error 對象的類型,我們再來定義一個Error 對象的子類 TimeoutError。
擴展知識:定製Error對象
Error 對象是ECMAScript的內建(build in)對象。
但是由於stack trace等原因我們不能完美的創建一個繼承自 Error 的類,不過在這裏我們的目的只是爲了和Error有所區別,我們將創建一個 TimeoutError 類來實現我們的目的。
在ECMAScript6中可以使用 class 語法來定義類之間的繼承關係。
class MyError extends Error{
// 繼承了Error類的對象
}
爲了讓我們的 TimeoutError 能支持類似 error instanceof TimeoutError 的使用方法,我們還需要進行如下工作。
//TimeoutError.js
function copyOwnFrom(target, source) {
Object.getOwnPropertyNames(source).forEach(function (propName) {
Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
});
return target;
}
function TimeoutError() {
var superInstance = Error.apply(null, arguments);
copyOwnFrom(this, superInstance);
}
TimeoutError.prototype = Object.create(Error.prototype);
TimeoutError.prototype.constructor = TimeoutError;
我們定義了 TimeoutError 類和構造函數,這個類繼承了Error的prototype。
它的使用方法和普通的 Error 對象一樣,使用 throw 語句即可,如下所示。
var promise = new Promise(function(){
throw TimeoutError("timeout");});promise.catch(function(error){ console.log(error instanceof TimeoutError);// true});
有了這個 TimeoutError 對象,我們就能很容易區分捕獲的到底是因爲超時而導致的錯誤,還是其他原因導致的Error對象了。