Promise 是一種處理異步的思路逾礁。
Promise
也是一個類敞斋。當我們說一個 promise 的時候植捎,一般指一個Promise
對象焰枢。
快速感受
傳統(tǒng)使用回調(diào)的寫法:
fs.readFile('config.json', function (error, text) {
if (error) {
console.error('Error while reading config file')
} else {
try {
const obj = JSON.parse(text)
console.log(JSON.stringify(obj, null, 4))
} catch (e) {
console.error('Invalid JSON in file')
}
}
})
使用 Promise 的寫法:
function readFilePromisified(filename) {
return new Promise(function (resolve, reject) {
fs.readFile(filename, { encoding: 'utf8' }, (error, data) => {
if (error) {
reject(error)
} else {
resolve(data)
}
})
})
}
readFilePromisified('config.json').then(function (text) { // (A)
const obj = JSON.parse(text)
console.log(JSON.stringify(obj, null, 4))
}).catch(function (error) { // (B)
// File read error or JSON SyntaxError
console.error('An error occurred', error)
})
其中 readFilePromisified
方法可以通過庫快速改寫,如通過 bluebird 庫:
const readFilePromisified = bluebird.promisify(fs. fs.readFile)
優(yōu)點
這里先提幾個關鍵點荐绝,具體優(yōu)點還需要邊學邊做體會避消。
使用 Promise 代替回調(diào)等機制,處于兩類目的监憎。一類是 Promise 比相應寫法更好鲸阔,簡練方便褐筛,清晰叙身,表達力強等等曲梗。第二類是相應機制的一些功能虏两,如重復回調(diào)定罢、錯誤處理祖凫、回調(diào)注冊時機惠况、組合多個異步操作等稠屠,非常容易出問題或非常難寫权埠,而 Promise 可以規(guī)避這類問題攘蔽。
最后一點是 Promise 統(tǒng)一了標準化的寫法满俗。回調(diào)并沒有統(tǒng)一的標準瓜富,Node.js 的回調(diào)与柑,XMLHttpRequest 的回調(diào)并不統(tǒng)一价捧。但 Promise 是統(tǒng)一的结蟋。
狀態(tài)
一個 Promise 會處于下面三種狀態(tài)中的一種:
- 異步結果就緒前渔彰,Promise 處于 pending 狀態(tài)
- 獲得結果后恍涂,Promise 處于 fulfilled 狀態(tài)
- 如果發(fā)生了錯誤再沧,Promise 處于 rejected 狀態(tài)。
Promise 落定(settled)淤堵,指它已獲得了結果或發(fā)生了錯誤(fulfilled 或 rejected)拐邪。Promise 一旦落定隘截,狀態(tài)不會再改變技俐。
Promise 提供兩種接口使 Promise 從進行中的狀態(tài)(pending)達到落定狀態(tài)(settled):
- 一類是“拒絕”(reject)方法雕擂,讓 Promise 進入 rejected 狀態(tài)井赌。
- 一類是“解決”(resolve)方法贵扰。如果解決的值不是一個 Promise戚绕,則進入 fulfilled 狀態(tài)舞丛;否則如果解決的值本身又是一個 Promise球切,則要等這個 Promise 達到落定狀態(tài)吨凑。
創(chuàng)建 Promise
通過構造器
const p = new Promise(function (resolve, reject) { // (A)
···
if (···) {
resolve(value) // success
} else {
reject(reason) // failure
}
})
并且如果在函數(shù)中拋出了異常鸵钝,p
自動被該異常拒絕恩商。
const p = new Promise(function (resolve, reject) {
throw new Error("Bad")
})
p.catch(function(e) { // 將執(zhí)行這里
console.error(e)
})
thenable
thenable 是一個對象,具有類似與 Promise 的 then()
方法韧献。thenable 不是一個 Promise锤窑。它一般是 Promise 標準化之前出現(xiàn)的采用 Promise 思想的自定義的對象渊啰。通過 Promise.resolve(x)
可以將一個非標準化的 thenable 轉換為一個 Promise绘证,見下一節(jié)嚷那。
Promise.resolve(x)
對于 x
不是一個 Promise,Promise.resolve(x)
返回一個 Promise腐泻,該 Promise 立即以 x
的值解決派桩。
Promise.resolve('abc')
.then(x => console.log(x)) // abc
如果 x
是一個 Promise,則 Promise.resolve(x)
原樣返回 x
范嘱。
const p = new Promise(() => null)
console.log(Promise.resolve(p) === p) // true
如果 x
是一個 thenable彤侍,則把它轉換為一個 Promise逆趋。
const fulfilledThenable = {
then(reaction) {
reaction('hello')
}
}
const promise = Promise.resolve(fulfilledThenable)
console.log(promise instanceof Promise) // true
promise.then(x => console.log(x)) // hello
總結:通過 Promise.resolve()
將任何值轉換為一個 Promise名斟。
Promise.reject(err)
Promise.reject(err)
返回一個 Promise魄眉,以 err
拒絕:
const myError = new Error('Problem!')
Promise.reject(myError)
.catch(err => console.log(err === myError)) // true
例子
1岩梳、fs.readFile()
import {readFile} from 'fs'
function readFilePromisified(filename) {
return new Promise(function (resolve, reject) {
readFile(filename, { encoding: 'utf8' }, (error, data) => {
if (error) {
reject(error)
} else {
resolve(data)
}
})
})
}
2晃择、XMLHttpRequest
function httpGet(url) {
return new Promise(function (resolve, reject) {
const request = new XMLHttpRequest()
request.onload = function () {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
request.onerror = function () {
reject(new Error('XMLHttpRequest Error: '+this.statusText))
}
request.open('GET', url)
request.send()
})
}
3列疗、延時
Let’s implement setTimeout()
as the Promise-based function delay()
(similar to Q.delay()).
function delay(ms) {
return new Promise(function (resolve, reject) {
setTimeout(resolve, ms)
})
}
// Using delay():
delay(5000).then(function () {
console.log('5 seconds have passed!')
})
4抵栈、超時
如果一個 Promise 在指定時間內(nèi)獲取到了結果(落定)古劲,則通知這個結果绢慢,否則以超時異常拒絕:
function timeout(ms, promise) {
return new Promise(function (resolve, reject) {
promise.then(resolve)
setTimeout(function () {
reject(new Error('Timeout after '+ms+' ms'))
}, ms)
})
}
消費一個 Promise
通過 then
和 catch
注冊處理方法骚露,在 Promise 解決或拒絕時調(diào)用棘幸。
promise
.then(value => { /* fulfillment */ })
.catch(error => { /* rejection */ })
使用回調(diào)的一個常見問題時误续,還來不及注冊回調(diào)函數(shù)蹋嵌,異步執(zhí)行就結束了栽烂。但 Promise 沒有這個問題恋脚。如果一個 Promise 對象落定了,會保持住狀態(tài)以及解決的值或拒絕的異常怀喉。此時再使用 then
或 catch
注冊處理方法仍可以得到結果躬拢。
then
還有一個兩參數(shù)的寫法:
promise.then(
null,
error => { /* rejection */ })
promise.catch(
error => { /* rejection */ })
傳遞
then()
方法會返回一個新的 Promise聊闯,于是你可以鏈接調(diào)用:
const q = Promise.resolve(true).then(function() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve("Good")
}, 1000)
})
})
q.then(function(result) {
console.log(result) // Good
})
上述代碼其實先后會產(chǎn)生 4 個 Promise:
const q1 = Promise.resolve(true)
const q2 = q1.then(function() {
const q3 = new Promise(function(resolve) {
setTimeout(function() {
resolve("Good")
}, 1000)
})
return q3
})
const q4 = q2.then(function(result) {
console.log(result)
})
具體來說域慷,.then(onFulfilled, onRejected)
返回的 Promise P 的值取決于它的回調(diào)函數(shù)的執(zhí)行犹褒。
1叠骑、如果 onFulfilled
或 onRejected
返回一個 Promise,該 Promise 的結論傳遞給 P茧跋。例子:
Promise.resolve(true)
.then(function (value1) {
return 123
})
.then(function (value2) {
console.log(value2) // 123
})
特別注意如果 onRejected
中正常返回了值瘾杭,則 then()
的結果是解決(fulfilled)而不是拒絕狀態(tài)√肿瑁可以利用這種特性恢復錯誤钝吮,提供默認值等搀绣。
Promise.reject("Bad")
.catch(function () {
return "I Know BAD"
})
.then(function (result) {
console.log(result)
})
2链患、如果 onFulfilled
或 onRejected
返回了一個值麻捻,值傳給 P贸毕。
該機制的主要作用是展平嵌套的 then()
調(diào)用,例子:
asyncFunc1()
.then(function (value1) {
asyncFunc2()
.then(function (value2) {
···
})
})
展平了的版本:
asyncFunc1()
.then(function (value1) {
return asyncFunc2()
})
.then(function (value2) {
···
})
3摊腋、如果 onFulfilled
或 onRejected
拋出了異常兴蒸,則 P 以該異常拒絕。
asyncFunc()
.then(function (value) {
throw new Error()
})
.catch(function (reason) {
// Handle error here
})
鏈式調(diào)用有一個好處,可以最后統(tǒng)一處理錯誤钓觉。中間環(huán)節(jié)的任何錯誤可以被最終統(tǒng)一處理:
asyncFunc1()
.then(asyncFunc2)
.then(asyncFunc3)
.catch(function (reason) {
// Something went wrong above
})
串行與并行
通過 then
鏈式調(diào)用異步函數(shù),這些函數(shù)的執(zhí)行是串行的:
asyncFunc1()
.then(() => asyncFunc2())
如果不使用 then
連接卧晓,它們是并行執(zhí)行的逼裆,但是你拿不到結果胜宇,也不知道什么時候他們?nèi)客瓿伞?/p>
asyncFunc1()
asyncFunc2()
解決方法是使用 Promise.all()
。它的參數(shù)是一個數(shù)組恢着,數(shù)組元素是 Promise桐愉。它返回一個 Promise,解析的結果是一個數(shù)組掰派。
Promise.all([ asyncFunc1(), asyncFunc2() ])
.then(([result1, result2]) => {
···
})
.catch(err => {
// Receives first rejection among the Promises
···
})
如果 map
的映射函數(shù)返回一個 Promise从诲,map
產(chǎn)生的數(shù)組由 Promise.all()
處理:
const fileUrls = [
'http://example.com/file1.txt',
'http://example.com/file2.txt',
]
const promisedTexts = fileUrls.map(httpGet)
Promise.all(promisedTexts)
.then(texts => {
for (const text of texts) {
console.log(text)
}
})
.catch(reason => {
// Receives first rejection among the Promises
})
Promise.race()
與 Promise.all()
類似,但只要數(shù)組中一個 Promise 落定(不管解決還是拒絕)靡羡,該 Promise 的結果作為 Promise.race()
的結果系洛。
注意如果數(shù)組為空,Promise.race()
永遠不會落定(settled)略步。
例子描扯,通過 Promise.race()
實現(xiàn)超時:
Promise.race([
httpGet('http://example.com/file.txt'),
delay(5000).then(function () {
throw new Error('Timed out')
})
])
.then(function (text) { ··· })
.catch(function (reason) { ··· })
常見錯誤
丟失 then
的結果
先看錯誤的代碼:
function foo() {
const promise = asyncFunc()
promise.then(result => {
···
})
return promise
}
再看正確的代碼:
function foo() {
const promise = asyncFunc()
return promise.then(result => {
···
})
}
甚至再簡化為:
function foo() {
return asyncFunc()
.then(result => {
···
})
}
不要忘了 promise.then(
也會產(chǎn)生一個結果,甚至可能拋出異常玫鸟,或返回一個異步結果(一個新的 Promise)贾费。
捕獲全部異常
前面提過可以通過鏈式調(diào)用,最后統(tǒng)一處理異常,這么做不但省事,而且可以避免遺漏錯誤:
asyncFunc1()
.then(
value => { // (A)
doSomething() // (B)
return asyncFunc2() // (C)
},
error => { // (D)
···
})
上面的代碼票从,D 處的錯誤處理只能處理 asyncFunc1() 的錯誤吟榴,但無法處理 B 處拋出的移除和 C 處返回的拒絕的 Promise宪拥。正確的寫法:
asyncFunc1()
.then(value => {
doSomething()
return asyncFunc2()
})
.catch(error => {
···
})
忽視非異步代碼的錯誤
有時我們編寫的異步方法中 —— 這里的異步方法指返回 Promise 的方法 —— 在異步前存在部分非異步的代碼。如果這些代碼拋出異常,異常將直接從方法拋出,而不是進入返回 Promise 并拒絕襟己,例如下面代碼的 A 處:
function asyncFunc() {
doSomethingSync() // (A)
return doSomethingAsync()
.then(result => {
···
})
}
解決方法一毒涧,捕獲并顯式拒絕:
function asyncFunc() {
try {
doSomethingSync()
return doSomethingAsync()
.then(result => {
···
})
} catch (err) {
return Promise.reject(err)
}
}
解決方法二,通過 Promise.resolve().then()
開始一段 Promise 鏈,將同步代碼包裹進 then
的方法中:
function asyncFunc() {
return Promise.resolve().then(() => {
doSomethingSync()
return doSomethingAsync()
}).then(result => {
···
})
}
方法三:
function asyncFunc() {
return new Promise((resolve, reject) => {
doSomethingSync()
resolve(doSomethingAsync())
})
.then(result => {
···
})
}
This approach saves you a tick (the synchronous code is executed right away), but it makes your code less regular.
finally
有時不管成功還是失敗都要執(zhí)行一些方法枣申,例如確認對話框,不管確定還是取消都要關閉對話框榨咐。Promise 原生 API 沒有提供 finally 的功能,但我們可以模擬:
Promise.prototype.finally = function (callback) {
const P = this.constructor
// We don’t invoke the callback in here,
// because we want then() to handle its exceptions
return this.then(
// Callback fulfills => continue with receiver’s fulfillment or rejection
// Callback rejects => pass on that rejection (then() has no 2nd parameter!)
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
)
}
調(diào)用:
createResource(···)
.then(function (value1) {
// Use resource
})
.then(function (value2) {
// Use resource
})
.finally(function () {
// Clean up
})
Promise 庫
原生 Promise 庫夠用但不夠強大遂蛀,對于更復雜的功能,可以使用某個第三方 Promise 庫裕坊。比如 bluebird。