Promise
Promise
的前世此生Promise
最先出如今 1988 年,由 Barbara Liskov、Liuba Shrira 獨創(論文:Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems)。而且在語言 MultiLisp 和 Concurrent Prolog 中已經有了相似的實現。html
JavaScript 中,Promise
的流行是得益於 jQuery 的方法 jQuery.Deferred()
,其餘也有一些更精簡獨立的 Promise
庫,例如:Q、When、Bluebird。java
# Q / 2010
import Q from 'q'
function wantOdd () {
const defer = Q.defer()
const num = Math.floor(Math.random() * 10)
if (num % 2) {
defer.resolve(num)
} else {
defer.reject(num)
}
return defer.promise
}
wantOdd()
.then(num => {
log(`Success: ${num} is odd.`) // Success: 7 is odd.
})
.catch(num => {
log(`Fail: ${num} is not odd.`)
})
複製代碼
因爲 jQuery 並無嚴格按照規範來制定接口,促使了官方對 Promise
的實現標準進行了一系列重要的澄清,該實現規範被命名爲 Promise/A+。後來 ES6(也叫 ES2015,2015 年 6 月正式發佈)也在 Promise/A+ 的標準上官方實現了一個 Promise
接口。jquery
new Promise( function(resolve, reject) {...} /* 執行器 */ );
複製代碼
想要實現一個 Promise
,必需要遵循以下規則:ios
Promise
是一個提供符合標準的 then()
方法的對象。pending
,可以轉換成 fulfilled
或 rejected
狀態。fulfilled
或 rejected
狀態肯定,不再能轉換成其餘狀態。ECMAScript's Promise global is just one of many Promises/A+ implementations.git
主流語言對於 Promise
的實現:Golang/go-promise、Python/promise、C#/Real-Serious-Games/c-sharp-promise、PHP/Guzzle Promises、Java/IOU、Objective-C/PromiseKit、Swift/FutureLib、Perl/stevan/promises-perl。github
因爲 JavaScript 是單線程事件驅動的編程語言,經過回調函數管理多個任務。在快速迭代的開發中,由於回調函數的濫用,很容易產生被人所詬病的回調地獄問題。Promise
的異步編程解決方案比回調函數更加合理,可讀性更強。ajax
傳說中比較誇張的回調:編程
現實業務中依賴關係比較強的回調:axios
# 回調函數
function renderPage () {
const secret = genSecret()
// 獲取用戶令牌
getUserToken({
secret,
success: token => {
// 獲取遊戲列表
getGameList({
token,
success: data => {
// 渲染遊戲列表
render({
list: data.list,
success: () => {
// 埋點數據上報
report()
},
fail: err => {
console.error(err)
}
})
},
fail: err => {
console.error(err)
}
})
},
fail: err => {
console.error(err)
}
})
}
複製代碼
使用 Promise
梳理流程後:api
# Promise
function renderPage () {
const secret = genSecret()
// 獲取用戶令牌
getUserToken(token)
.then(token => {
// 獲取遊戲列表
return getGameList(token)
})
.then(data => {
// 渲染遊戲列表
return render(data.list)
})
.then(() => {
// 埋點數據上報
report()
})
.catch(err => {
console.error(err)
})
}
複製代碼
Promise
的運轉其實是一個觀察者模式,then()
中的匿名函數充當觀察者,Promise
實例充當被觀察者。
const p = new Promise(resolve => setTimeout(resolve.bind(null, 'from promise'), 3000))
p.then(console.log.bind(null, 1))
p.then(console.log.bind(null, 2))
p.then(console.log.bind(null, 3))
p.then(console.log.bind(null, 4))
p.then(console.log.bind(null, 5))
// 3 秒後
// 1 2 3 4 5 from promise
複製代碼
# 實現
const defer = () => {
let pending = [] // 充當狀態並收集觀察者
let value = undefined
return {
resolve: (_value) => { // FulFilled!
value = _value
if (pending) {
pending.forEach(callback => callback(value))
pending = undefined
}
},
then: (callback) => {
if (pending) {
pending.push(callback)
} else {
callback(value)
}
}
}
}
# 模擬
const mockPromise = () => {
let p = defer()
setTimeout(() => {
p.resolve('success!')
}, 3000)
return p
}
mockPromise().then(res => {
console.log(res)
})
console.log('script end')
// script end
// 3 秒後
// success!
複製代碼
Promise
怎麼用Promise
異步編程在 Promise
出現以前每每使用回調函數管理一些異步程序的狀態。
# 常見的異步 Ajax 請求格式
ajax(url, successCallback, errorCallback)
複製代碼
Promise
出現後使用 then()
接收事件的狀態,且只會接收一次。
案例:插件初始化。
使用回調函數:
# 插件代碼
let ppInitStatus = false
let ppInitCallback = null
PP.init = callback => {
if (ppInitStatus) {
callback && callback(/* 數據 */)
} else {
ppInitCallback = callback
}
}
// ...
// ...
// 經歷了一系列同步異步程序後初始化完成
ppInitCallback && ppInitCallback(/* 數據 */)
ppInitStatus = true
# 第三方調用
PP.init(callback)
複製代碼
使用 Promise:
# 插件代碼
let initOk = null
const ppInitStatus = new Promise(resolve => initOk = resolve)
PP.init = callback => {
ppInitStatus.then(callback).catch(console.error)
}
// ...
// ...
// 經歷了一系列同步異步程序後初始化完成
initOk(/* 數據 */)
# 第三方調用
PP.init(callback)
複製代碼
相對於使用回調函數,邏輯更清晰,何時初始化完成和觸發回調一目瞭然,再也不須要重複判斷狀態和回調函數。固然更好的作法是隻給第三方輸出狀態和數據,至於如何使用由第三方決定。
# 插件代碼
let initOk = null
PP.init = new Promise(resolve => initOk = resolve)
// ...
// ...
// 經歷了一系列同步異步程序後初始化完成
initOk(/* 數據 */)
# 第三方調用
PP.init.then(callback).catch(console.error)
複製代碼
then()
必然返回一個 Promise
對象,Promise
對象又擁有一個 then()
方法,這正是 Promise
可以鏈式調用的緣由。
const p = new Promise(r => r(1))
.then(res => {
console.log(res) // 1
return Promise.resolve(2)
.then(res => res + 10) // === new Promise(r => r(1))
.then(res => res + 10) // 因而可知,每次返回的是實例後面跟的最後一個 then
})
.then(res => {
console.log(res) // 22
return 3 // === Promise.resolve(3)
})
.then(res => {
console.log(res) // 3
})
.then(res => {
console.log(res) // undefined
return '最強王者'
})
p.then(console.log.bind(null, '是誰活到了最後:')) // 是誰活到了最後: 最強王者
複製代碼
因爲返回一個 Promise
結構體永遠返回的是鏈式調用的最後一個 then()
,因此在處理封裝好的 Promise
接口時不必在外面再包一層 Promise
。
# 包一層 Promise
function api () {
return new Promise((resolve, reject) => {
axios.get(/* 連接 */).then(data => {
// ...
// 經歷了一系列數據處理
resolve(data.xxx)
})
})
}
# 更好的作法:利用鏈式調用
function api () {
return axios.get(/* 連接 */).then(data => {
// ...
// 經歷了一系列數據處理
return data.xxx
})
}
複製代碼
Promise
實例Promise.all()
/ Promise.race()
能夠將多個 Promise 實例包裝成一個 Promise 實例,在處理並行的、沒有依賴關係的請求時,可以節約大量的時間。
function wait (ms) {
return new Promise(resolve => setTimeout(resolve.bind(null, ms), ms))
}
# Promise.all
Promise.all([wait(2000), wait(4000), wait(3000)])
.then(console.log)
// 4 秒後 [ 2000, 4000, 3000 ]
# Promise.race
Promise.race([wait(2000), wait(4000), wait(3000)])
.then(console.log)
// 2 秒後 2000
複製代碼
Promise
和 async
/ await
async
/ await
實際上只是創建在 Promise
之上的語法糖,讓異步代碼看上去更像同步代碼,因此 async
/ await
在 JavaScript 線程中是非阻塞的,但在當前函數做用域內具有阻塞性質。
let ok = null
async function foo () {
console.log(1)
console.log(await new Promise(resolve => ok = resolve))
console.log(3)
}
foo() // 1
ok(2) // 2 3
複製代碼
使用 async
/ await
的優點:
簡潔乾淨
寫更少的代碼,不須要特意建立一個匿名函數,放入 then()
方法中等待一個響應。
# Promise
function getUserInfo () {
return getData().then(
data => {
return data
}
)
}
# async / await
async function getUserInfo () {
return await getData()
}
複製代碼
條件語句
當一個異步返回值是另外一段邏輯的判斷條件,鏈式調用將隨着層級的疊加變得更加複雜,讓人很容易在代碼中迷失自我。使用 async
/ await
將使代碼可讀性變得更好。
# Promise
function getGameInfo () {
getUserAbValue().then(
abValue => {
if (abValue === 1) {
return getAInfo().then(
data => {
// ...
}
)
} else {
return getBInfo().then(
data => {
// ...
}
)
}
}
)
}
# async / await
async function getGameInfo () {
const abValue = await getUserAbValue()
if (abValue === 1) {
const data = await getAInfo()
// ...
} else {
// ...
}
}
複製代碼
中間值
異步函數經常存在一些異步返回值,做用僅限於成爲下一段邏輯的入場券,若是經歷層層鏈式調用,很容易成爲另外一種形式的「回調地獄」。
# Promise
function getGameInfo () {
getToken().then(
token => {
getLevel(token).then(
level => {
getInfo(token, level).then(
data => {
// ...
}
)
}
)
}
)
}
# async / await
async function getGameInfo() {
const token = await getToken()
const level = await getLevel(token)
const data = await getInfo(token, level)
// ...
}
複製代碼
靠譜的 await
await 'qtt'
等於 await Promise.resolve('qtt')
,await
會把任何不是 Promise
的值包裝成 Promise
,看起來貌似沒有什麼用,可是在處理第三方接口的時候能夠 「Hold」 住同步和異步返回值,不然對一個非 Promise
返回值使用 then()
鏈式調用則會報錯。
使用 async
/ await
的缺點:
async
永遠返回 Promise
對象,不夠靈活,不少時候我只想單純返回一個基本類型值。
await
阻塞 async
函數中的代碼執行,在上下文關聯性不強的代碼中略顯累贅。
# async / await
async function initGame () {
render(await getGame()) // 等待獲取遊戲執行完畢再去獲取用戶信息
report(await getUserInfo())
}
# Promise
function initGame () {
getGame()
.then(render)
.catch(console.error)
getUserInfo() // 獲取用戶信息和獲取遊戲同步進行
.then(report)
.catch(console.error)
}
複製代碼
鏈式調用中儘可能結尾跟 catch
捕獲錯誤,而不是第二個匿名函數。由於標準裏註明了若 then()
方法裏面的參數不是函數則什麼都不錯,因此 catch(rejectionFn)
其實就是 then(null, rejectionFn)
的別名。
anAsyncFn().then(
resolveSuccess,
rejectError
)
複製代碼
在以上代碼中,anAsyncFn()
拋出來的錯誤 rejectError
會正常接住,可是 resolveSuccess
拋出來的錯誤將沒法捕獲,因此更好的作法是永遠使用 catch
。
anAsyncFn()
.then(resolveSuccess)
.catch(rejectError)
複製代碼
假若講究一點,也能夠經過 resolveSuccess
來捕獲 anAsyncFn()
的錯誤,catch
捕獲 resolveSuccess
的錯誤。
anAsyncFn()
.then(
resolveSuccess,
rejectError
)
.catch(handleError)
複製代碼
經過全局屬性監聽未被處理的 Promise 錯誤。
瀏覽器環境(window
)的拒絕狀態監聽事件:
unhandledrejection
當 Promise 被拒絕,而且沒有提供拒絕處理程序時,觸發該事件。rejectionhandled
當 Promise 被拒絕時,若拒絕處理程序被調用,觸發該事件。// 初始化列表
const unhandledRejections = new Map()
// 監聽未處理拒絕狀態
window.addEventListener('unhandledrejection', e => {
unhandledRejections.set(e.promise, e.reason)
})
// 監聽已處理拒絕狀態
window.addEventListener('rejectionhandled', e => {
unhandledRejections.delete(e.promise)
})
// 循環處理拒絕狀態
setInterval(() => {
unhandledRejections.forEach((reason, promise) => {
console.log('handle: ', reason.message)
promise.catch(e => {
console.log(`I catch u!`, e.message)
})
})
unhandledRejections.clear()
}, 5000)
複製代碼
注意:Promise.reject()
和 new Promise((resolve, reject) => reject())
這種方式不能直接觸發 unhandledrejection
事件,必須是知足已經進行了 then()
鏈式調用的 Promise
對象才行。
Promise
當執行一個超級久的異步請求時,若超過了可以忍受的最大時長,每每須要取消這次請求,可是 Promise
並無相似於 cancel()
的取消方法,想結束一個 Promise
只能經過 resolve
或 reject
來改變其狀態,社區已經有了知足此需求的開源庫 Speculation。
或者利用 Promise.race()
的機制來同時注入一個會超時的異步函數,可是 Promise.race()
結束後主程序其實還在 pending
中,佔用的資源並無釋放。
Promise.race([anAsyncFn(), timeout(5000)])
複製代碼
若想按順序執行一堆異步程序,可以使用 reduce
。每次遍歷返回一個 Promise
對象,在下一輪 await
住從而依次執行。
function wasteTime (ms) {
return new Promise(resolve => setTimeout(() => {
resolve(ms)
console.log('waste', ms)
}, ms))
}
// 依次浪費 3 4 5 3 秒 === 15 秒
const arr = [3000, 4000, 5000, 3000]
arr.reduce(async (last, curr) => {
await last
return wasteTime(curr)
}, undefined)
複製代碼
Promise
。Promise
中全部方法的返回類型都是 Promise
。Promise
中的狀態改變是一次性的,建議在 reject()
方法中傳遞 Error
對象。Promise
添加 then()
和 catch()
方法。Promise.all()
行運行多個 Promise
。then()
或 catch()
後都作點什麼,可以使用 finally()
。then()
掛載在同一個 Promise
上。async
(異步)函數返回一個 Promise
,全部返回 Promise
的函數也能夠被視做一個異步函數。await
用於調用異步函數,直到其狀態改變(fulfilled
or rejected
)。async
/ await
時要考慮上下文的依賴性,避免形成沒必要要的阻塞。更多文章訪問個人博客