Promise 是異步編程的一種解決方案场航,比傳統(tǒng)的解決方案 —— 回調(diào)函數(shù)和事件 —— 更合理且強大
Promise尖啡,簡單來說就是一個容器,里面保存著某個未來才會結束的事件(通常是一個異步操作)的結果竭业。從語法上來說,Promise 是一個對象悦冀,從它可以獲取異步操作的消息。
Promise 對象有以下兩個特點:
-
對象的狀態(tài)不受外界影響睛琳。
Promise對象代表一個異步操作盒蟆,有 3 中狀態(tài)
- Pending(進行中)
- Fulfilled(已成功)
- Rejected(已失敗)
只有異步操作的結果可以決定當前是哪一種狀態(tài),任何其他操作都無法改變這個狀態(tài)师骗。(這也是 "promise" 這個名字的由來历等,它在英語中意思是“承諾”,表示其它手段無法改變)
-
一旦狀態(tài)改變就不會再變丧凤,任何時候都可以得到這個結果募闲。
Promise 對象的狀態(tài)改變只有兩種可能:
- 從 Pending 變?yōu)?Fulfilled
- 從 Pending 變?yōu)?Rejected
只要這兩種情況發(fā)生步脓,狀態(tài)就凝固了愿待,不會再變,而是一直保持這個結果靴患,這時就稱為 Resolved(已定性)
Promise 也有一些缺點:
- 無法取消 Promise仍侥,一旦新建它就會立即執(zhí)行,無法中途取消
- 如果不設置回調(diào)函數(shù)鸳君,Promise 內(nèi)部拋出錯誤不會反應到外部
- 當處于 Pending 狀態(tài)時农渊,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)
如果某些事件不斷地反復發(fā)生,一般來說或颊,使用 Stream 模式(nodejs.org/api/stream.html)
二砸紊、基本用法
ES6 規(guī)定,Promise 對象是一個構造函數(shù)囱挑,用來生成 Promise 實例醉顽。
創(chuàng)建一個Promise 實例
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */) {
resolve()
} else {
// 操作失敗
reject()
}
})
Promise 構造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是:
- resolve:將 Promise 對象的狀態(tài)從 “未完成” 變成 “成功”(即從 Pending 變?yōu)?Resolved)平挑,并將異步操作的結果作為參數(shù)傳遞出去
- reject:將Promise 對象的狀態(tài)從 “未完成” 變?yōu)?“失敗” (即從 Pending 變?yōu)?Rejected)游添,并將異步操作報出的錯誤作為參數(shù)傳遞出去
Promise 實例生成以后系草,可以用 then 方法分別制定 Resolved 狀態(tài) 和 Rejected 狀態(tài)的回調(diào)函數(shù)。
promise.then(function(value) {
// resolve
}, function(error) {
// reject
})
第一個回調(diào)函數(shù)是 Promise 對象的狀態(tài)變?yōu)?Resolved 時調(diào)用唆涝,第二個回調(diào)函數(shù)是 Promise 對象的狀態(tài)變?yōu)?Rejected 調(diào)用找都。
下面是一個 Promise 對象的簡單例子。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done')
})
}
timeout(100).then((value) => {
console.log(value)
})
Promise 新建后就會立即執(zhí)行
let promise = new Promise(function(resolve, reject) {
console.log('Promise')
resolve()
})
promise.then(function() {
console.log('Resolved.')
})
console.log('Hi!')
// Promise
// Hi
// Resolved
下面是異步加載圖片的例子
function loadImageAsync(url) {
return new Promise((resolve, reject) => {
var image = new Image()
image.onload = function() {
resolve(image)
}
image.onerror = function() {
reject(new Error('Could not load image at' + url))
}
image.src = url
})
}
用 Promise 對象實現(xiàn) AJAX 操作
var getJSON = function(url) {
var promise = new Promise((resolve, reject) => {
var client = new XMLHttpRequest()
client.open('GET', url)
client.onreadystatechange = handler
client.responseType = 'json'
client.setRequestHeader("Accept", "application/json")
client.send()
function handler() {
if (this.readyState !== 4) {
return
}
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
})
return promise
}
getJSON("/posts.json").then(json => {
console.log('Contents:' + json)
}, error => {
console.error('出錯了'廊酣,error)
})
如果調(diào)用 resolve 函數(shù) 和 reject 函數(shù)時帶有參數(shù)能耻,那么這些參會會被傳遞給回調(diào)函數(shù) reject 函數(shù)的參數(shù)通常是 Error 對象的實例,表示拋出的錯誤亡驰,resolve 函數(shù)的參數(shù)處理正常的值外嚎京,還可能是另外一個 Promise 實例。
var p1 = new Promise((resolve, reject) => {
// ...
})
var p2 = new Promise((resolve, reject) => {
// ...
resolve(p1)
})
p2 的 resolve方法將 p1 作為參數(shù)隐解,即一個異步操作的結果是返回另一個異步操作
此時 p1 的狀態(tài)就會傳遞給p2鞍帝。也就是說,p1的狀態(tài)決定了 p2 的狀態(tài)煞茫。如果 p1 的狀態(tài)時 pending帕涌,那么 p2 的回調(diào)函數(shù)就會等待 p1 的狀態(tài)改變;如果 p1 的狀態(tài)已經(jīng)是 Resolved 或 Rejected续徽,那么 p2 的回調(diào)函數(shù)將會立即執(zhí)行
var p1 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('fail')), 3000)
})
var p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
上面的代碼中蚓曼,p1 是一個 Promise,3秒之后變成了 rejected钦扭。p2 的狀態(tài)在 1 秒之后改變纫版,resolve 方法返回的是 p1,。由于 p2 返回的是另一個 Promise客情,導致 p2 的狀態(tài)無效其弊,由 p1 的狀態(tài)決定 p2 的狀態(tài)。所以膀斋,后面的 then 語句都變成針對后者(p1)的梭伐。再過2秒,p1 變成 Rejected仰担,觸發(fā) catch 方法指定的回調(diào)函數(shù)
注意糊识,調(diào)用 resolve 或 reject 并不會終止 Promise 的參數(shù)函數(shù)的執(zhí)行
new Promise((resolve, reject) => {
resolve(1)
console.log(2)
}).then(r => {
console.log(r)
})
// 2
// 1
因為 立即 resolve 的 Promise 是在本輪事件循環(huán)的末尾執(zhí)行,總是晚于本輪循環(huán)的同步任務
一般來說摔蓝,調(diào)用 resolve 或 reject 之后赂苗,Promise 的使命就完成了。所以贮尉,最好在他們前面加上 return 語句拌滋,這樣就不會產(chǎn)生意外了。
new Promise((resolve, reject) => {
return resolve(1)
// 后面的語句不會執(zhí)行
console.log(2)
})
三绘盟、Promise.prototype.then()
Promise 實例具有 then 方法鸠真,即 then 方法是定義在原型對象 Promise.prototype 上的悯仙。它的作用是為 Promise 實例添加狀態(tài)改變時的回調(diào)函數(shù)否灾。
第一個參數(shù)是 Resolved 狀態(tài)的回調(diào)函數(shù)
第二個參數(shù)(可選) 是 Rejected 狀態(tài)的回調(diào)函數(shù)
then 方法返回的是一個新的 Promise 實例(注意嗽仪,不是原來那個 Promise 實例)。因此可以采用鏈式寫法来破,即 then 方法后面再調(diào)用另一個 then 方法祭隔。
getJSON("/posts.json").then(function(json) {
return json.post
}).then(function(post) {
// ...
})
采用鏈式的 then 可以指定一組按照次序調(diào)用的回調(diào)函數(shù)货岭。這時,前一個回調(diào)函數(shù)有可能返回的還是一個 Promise 對象(即有異步操作)疾渴,而后一個回調(diào)函數(shù)就會等待該 Promise 對象的狀態(tài)發(fā)生變化千贯,再次調(diào)用
getJSON('/post/1.json').then(
post => getJSON(post.commentURL)
).then(
comments => console.log('Resolved:', comments),
err => console.log('Rejected:', err)
)
四、Promise.prototype.catch()
Promise.prototype.catch 方法是 .then(null, rejection) 的別名搞坝,用于指定發(fā)生錯誤時的回調(diào)函數(shù)搔谴。
getJSON('/posts.json').then(posts => {
// ...
}).catch(error => {
// 處理 getJSON 和前一個回調(diào)函數(shù)運行時發(fā)生的錯誤
console.log('err', error)
})
此外,then 方法指定的回調(diào)函數(shù)如果在運行中拋出錯誤桩撮,也會被 catch 方法捕獲
Promise 爬出一個錯誤也會被 catch 方法指定的回調(diào)函數(shù)所捕獲
var promise = new Promise((resolve, reject) => {
throw new Error('test')
})
promise.catch(error => {
console.log(error)
})
// Error: test
/* 上面的寫法等同于下面的兩種寫法 */
// 寫法一
var promise = new Promise((resolve, reject) => {
try {
throw new Error('test')
} catch(e) {
reject(e)
}
})
promise.catch(error => {
console.log(error)
})
// 寫法二
var promise = new Promise((resolve, reject) => {
reject(new Error('test'))
})
promise.catch(error => {
console.log(error)
})
如果 Promise 狀態(tài)已經(jīng)變成 Resolved敦第,再拋出錯誤是無效的
var promise = new Promise((resolve, reject) => {
resolve('ok')
throw new Error('test')
})
promise
.then(value => { console.log(value) })
.catch(error => { console.log(error) })
// ok
因為 Promise 的狀態(tài)一旦改變,就會永久保存該狀態(tài)店量,不會再改變
Promise 對象的錯誤具有"冒泡"性質(zhì)芜果,會一直向后傳遞,直到被捕獲為止融师。也就是說右钾,錯誤總是會被下一個 catch 語句捕獲。
getJSON('/post/1.json')
.then(post => getJSON(post.commentURL))
.then(comments => { /*some code */ })
.catch(error => {
// 處理前面 3 個Promise 產(chǎn)生的錯誤
})
上面的代碼中旱爆,一共3個 Promise 對象:一個由 getJSON 產(chǎn)生舀射,兩個 由 then 產(chǎn)生。其中任何一個拋出的錯誤都會被最后一個 catch 捕獲疼鸟。
一般來說后控,不要再 then 方法中定義 Rejected 狀態(tài)的回調(diào)函數(shù) (即 then 的第二個參數(shù)),而應總是使用 catch 方法
如果沒有使用 catch 方法指定錯誤處理的回調(diào)函數(shù)空镜,Promise 對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應
var someAsyncThing = function() {
return new Promise((resolve, reject) => {
// 下面一行會報錯捌朴,因為 x 沒有聲明
resolve(x + 2)
})
}
someAsyncThing()
.then(() => {
console.log('everything is great')
})
瀏覽器此時會打印出錯誤 “ReferenceError:x is not defined”吴攒,不過不會總之腳本執(zhí)行
需要注意的是,catch 方法返回的還是一個 Promise 對象砂蔽,因此后面還可以調(diào)用 then 方法洼怔。
var someAsyncThing = function() {
return new Promise((resolbe, reject) => {
// 下面一行會報錯,因為 x 沒有聲明
resolve(x + 2)
})
}
someAsyncThing()
.catch(error => {
console.log('oh on', error)
})
.then(() => {
console.log('carry on')
})
// oh on ReferenceError: resolve is not defined
// carry on
如果沒有報錯左驾,則會跳過 catch 方法
Promise.resolve()
.catch(error => {
console.log('oh on', error)
})
.then(() => {
console.log('carry on')
})
// carry on
如果上面代碼中镣隶,then 方法里面報錯极谊,就與前面的 catch 無關了
catch 中還能再拋出錯誤,如果后面沒有別的 catch 方法安岂,導致這個錯誤不會被捕獲轻猖,也不會傳遞到外層。
someAsyncThing()
.then(() => someOtherAsyncThing())
.catch(error => {
console.log('oh on', error)
// 下面操作會報錯域那,因為 y 沒有聲明
y + 2
})
.catch(error => {
console.log('carry on', error)
})
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError:y is not defined]
五咙边、Promise.all()
Promise.all 方法用于將多個 Promise 實例 包裝成一個新的 Promise 實例
var p = Promise.all([p1, p2, p3])
Promise.all 方法接受一個數(shù)組作為參數(shù),p1次员、p2败许、p3 都是 Promise 對象的實例(Promise.all 方法的參數(shù)不一定是數(shù)組,但是必須具有 Iterator 接口淑蔚,且返回的每個成員都是 Promise 實例)
p 的狀態(tài) 由 p1市殷、p2、p3決定刹衫,分成兩種情況:
- 只有 p1被丧、p2、p3 的狀態(tài)都變成 Fulfilled绪妹,p 的狀態(tài)才會變成 Fulfilled甥桂,此時 p1、p2邮旷、p3的返回值組成一個數(shù)組黄选,傳遞給 P 的回調(diào)函數(shù)。
- 只要 p1婶肩、p2办陷、p3 中有一個被 Rejected,p 的狀態(tài)就變成Rejected律歼,此時第一個被 Rejected 的實例的返回值會傳遞給 p 的回調(diào)函數(shù)
// 生成一個 Promise 對象的數(shù)組
var promise = [2, 3, 5, 7, 1,, 13].map(id => {
return getJSON('/post/' + id + '.json')
})
Promise.all(promise).then(posts => {
// ...
}).catch(reason => {
// ...
})
如上民镜,即是 包含6個 Promise 實例的數(shù)組
下面是另一個例子
const databasePromise = connectDatabase()
const booksPromise = databasePromise
.then(findAllBooks)
const userPromise = databasePromise
.then(getCurrentUser)
Promise.all([
booksPromise,
userPromise
])
.then(([books, user]) => pickTopRecommentations(books, user))
booksPromise 和 userPromise 是兩個異步操作,只有他們的結果都返回险毁,才會觸發(fā) PickTopRecmmentations 回調(diào)函數(shù)
如果作為參數(shù)的 Promise 實例自身定義了 catch 方法制圈,那么 它被 rejected 是并不會觸發(fā) Promise.all() 的 catch 方法
const p1 = new Promise((resolve, reject) => {
resolve('hello')
})
.then(result => result)
.catch(e => e)
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了')
})
.then(result => result)
.catch(e => e)
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e))
// ["hello", Error: 報錯了]
上面代碼中,p2 首先會 rejected畔况,但是 p2 有自己的 catch 方法鲸鹦,該方法返回的是一個新的 Promise 實例,p2 實際上指向的是這個實例跷跪。該實例執(zhí)行完 catch 方法后也會變成 resolved馋嗜,導致 Promise.all() 方法參數(shù)里面的兩個實例都會 resolved,因此會調(diào)用 then 方法指定的回調(diào)函數(shù)吵瞻,而不會調(diào)用 catch 方法指定的回調(diào)函數(shù)葛菇。
如果 p2 沒有自己的 catch 方法甘磨,就會調(diào)用 Promise.all() 的 catch 方法
六、Promise.race()
Promise.race 方法同樣是將多個 Promise 實例包裝成一個新的 Promise 實例眯停。Promise.race 方法的參數(shù)與 Promise.all 方法一樣
var p = Promise.race([p1, p2, p3)
只要 p1济舆、p2庵朝、p3 中有一個實例率先改變狀態(tài)吗冤,p 的狀態(tài)就跟著改變九府。那個率先改變的 Promise 實例的返回值就傳遞給 P 的回調(diào)函數(shù)
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
])
p.then(response => console.log(response))
p.catch(error => console.log(error))
上面的代碼中,如果 5秒之內(nèi) fetch 方法無法返回結果侄旬,變量 p 的狀態(tài)就會變?yōu)?Rejected肺蔚,從而觸發(fā) catch 方法指定的回調(diào)函數(shù)
七、Promise.resolve()
有時候需要將現(xiàn)有對象轉(zhuǎn)為 Promise 對象宣羊,Promise.resolve 方法就起到這個作用
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
Promise.resolve 等價于下面的寫法
Promise.resolve('foo')
// 等價于
new Promise( resolve => resolve('foo'))
Promise.resolve 方法的參數(shù)分成以下4種情況:
1. 參數(shù)是一個 Promise 實例
如果參數(shù)是 Promise 實例,那么 Promise.resolve 將不做任何修改仇冯,原封不動地返回這個實例
2. 參數(shù)是一個 thenable 對象
thenable 對象值的是具體 then 方法的對象,比如下面這個對象
let thenable = {
then(resolve, reject) {
resolve(42)
}
}
Promise.resolve 方法會將這個對象轉(zhuǎn)為 Promise 對象族操,然后立即執(zhí)行 thenable 對象的 then 方法苛坚。
let thenable = {
then(resolve, reject) {
resolve(42)
}
}
let p1 = Promise.resolve(thenable)
p1.then(value => {
console.log(value) // 42
})
thenable 對象 then 方法執(zhí)行后,對象 p1 的狀態(tài)就變?yōu)?resolved 從而立即執(zhí)行最后的 then 方法指定的回調(diào)函數(shù)泼舱,輸出 42。
3. 參數(shù)不是具有 then 方法的對象或根本不是對象
如果參數(shù)是一個原始值娇昙,或者是一個不具有 then 方法的對象笤妙,那么 Promise.resolve 方法返回一個新的 Promise 對象冒掌,狀態(tài)為 Resolved
var p = Promise.resolve('Hello')
p.then(s => {
console.log(s)
})
// Hello
上面生成了一個新的 Promise 對象的實例p。由于字符串 Hello 不屬于異步操作(判斷方法是 字符串對象 不具有 then 方法)宋渔,返回 Promise 實例的狀態(tài)從生成起就是 Resolved,所以回調(diào)函數(shù)會立即執(zhí)行。Promise.resolve 方法的參數(shù)會同時傳給回調(diào)函數(shù)严蓖。
4. 不帶有任何參數(shù)
Promise.resolve 方法允許在調(diào)用時不帶有參數(shù)氧急,而直接返回一個 Resolved 狀態(tài)的 Promise 對象毫深。
var p = Promise.resolve()
p.then(() => {
// ....
})
需要注意的是,立即 resolve 的 Promise 對象是在本輪“事件循環(huán)”(event loop) 結束時哑蔫,而不是在下一輪“事件循環(huán)”開始時。
setTimeout(() => {
console.log('three')
}, 0)
Promise.resolve().then(() => {
console.log('two')
})
console.log('one')
上面的代碼中闸迷,setTimeout(fn, 0) 是在下一輪“事件循環(huán)” 開始時執(zhí)行的,Promise.resolve() 在本輪“事件循環(huán)”結束時執(zhí)行逮走,console.log('one')則是立即執(zhí)行
八今阳、Promise.reject()
Promise.reject(reason) 方法也會返回一個新的 Promise 實例师溅,狀態(tài)為 Rejected
var p = Promise.reject('出錯了')
// 等同于
var p = new Promise((resolve, reject) => reject('出錯了'))
p.then(null, s => {
console.log(s)
})
// 出錯了
Promise.reject() 方法的參數(shù)會原封不動地作為 reject 的理由變成后續(xù)方法的參數(shù)盾舌。這一點與 Promise.resolve() 方法不一致。
const thenable = {
then(resolve, reject) {
reject('出錯了')
}
}
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
上面的代碼中窿锉,Promise.reject 方法的參數(shù)是一個 thenable 對象窖维,執(zhí)行以后榆综,后面 catch 方法的參數(shù)不是 reject 拋出的 “出錯了” 這個字符串铸史,而是 thenable 對象
九、兩個有用的附加方法
ES6 的 Promise API 提供的方法不是很多琳轿,可以自己部署一些有用的方法。下面部署兩個不在 ES6 中但很有用的方法崭篡。
9.1、done()
無論 Promise 對象的回調(diào)鏈以 then 方法還是 catch 方法結尾迹炼,只要最后一個方法拋出錯誤,都有可能無法捕捉到(因為 Promise 內(nèi)部的錯誤不會冒泡到全局)斯入。為此,我們可以提供一個 done 方法刻两,它總是處于回調(diào)鏈的尾端,保證拋出任何可能出現(xiàn)的錯誤磅摹。
asyncFunc()
.then(f1)
.catch(r1)
.then(f2)
.done()
它的實現(xiàn)代碼相當簡單
Promise.prototype.done = function(onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(reason => {
// 拋出一個全局錯誤
setTimeout(() => { throw reason }, 0)
})
}
done 方法可以像 then 方法那樣使用,提供 Fulfilled 和 Rejected 狀態(tài)的回調(diào)函數(shù)饼灿,也可以不停任何參數(shù)。但不管怎么樣赔退,done 方法都會捕捉到 任何可能出現(xiàn)的錯誤证舟,并向全局拋出
9.2硕旗、finally()
finally 方法用于指定不管 Promise 對象最后狀態(tài)如何都會執(zhí)行的操作女责。它與 done 方法的最大區(qū)別在于,他接受一個普通的回調(diào)函數(shù)作為參數(shù)抵知,該函數(shù)不管怎樣都必須執(zhí)行。
server.listen(0)
.then(() => {
// run test
})
.finally(server.stop)
上面的例子刷喜,服務器使用 Promise 處理請求,然后使用 finally 方法關掉服務器
它的實現(xiàn)也很簡單
Promise.prototype.finally = function(callback) {
let p = this.constructor
return this.then(
value => p.resolve(callback()).then(() => value),
reason => p.resolve(callback()).then(() => { throw reason })
)
}
不管前面的 Promise 是 fulfilled 還是 rejected初茶,都會執(zhí)行回調(diào)函數(shù) callback
十浊闪、應用
10.1恼布、加載圖片
const prelpadImage = function(path) {
return new Promise(function (resolve, reject) {
var image = new Image()
image.onload = resolve
image.onerror = reject
image.src = path
})
}
10.2 Generator 函數(shù)與 Promise 的結合
使用 Generator 函數(shù)管理流程,遇到異步操作時通常返回一個 Promise 對象
function getFoo() {
return new Promise(function (resolve, reject) {
resolve('foo')
})
}
var g = function* () {
try {
var foo = yield getFoo()
console.log(foo)
} catch(e) {
console.log(e)
}
}
function run(generator) {
var it = generator()
function go(result) {
if (result.done) return result.value
return result.value.then( function(value) {
return go(it.next(value))
}, function (error) {
return go(it.throw(error))
})
}
go(it.next())
}
run(g)
Generator 函數(shù)g 中有一個異步操作getFoo折汞,它返回的就是一個 Promise 對象盖腿。函數(shù) run 用來處理這個 Promise 對象,并調(diào)用下一個 next 方法。