一、什麼是 Promise 1.1 Promise 的前世今生 Promise 最早出現在 1988 年,由 Barbara Liskov、Liuba Shrira 首創(論文:Promises: Linguistic Support for Efficient Asynchronous Proce ...
一、什麼是 Promise
1.1 Promise
的前世今生
Promise
最早出現在 1988 年,由 Barbara Liskov、Liuba Shrira 首創(論文:Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems)。並且在語言 MultiLisp 和 Concurrent Prolog 中已經有了類似的實現。
JavaScript 中,Promise
的流行是得益於 jQuery 的方法 jQuery.Deferred()
,其他也有一些更精簡獨立的 Promise
庫,例如:Q、When、Bluebird。
# 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
介面。
new Promise( function(resolve, reject) {...} /* 執行器 */ );
想要實現一個 Promise
,必須要遵循如下規則:
Promise
是一個提供符合標準的then()
方法的對象。- 初始狀態是
pending
,能夠轉換成fulfilled
或rejected
狀態。 - 一旦
fulfilled
或rejected
狀態確定,再也不能轉換成其他狀態。 - 一旦狀態確定,必須要返回一個值,並且這個值是不可修改的。
ECMAScript's Promise global is just one of many Promises/A+ implementations.
主流語言對於 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。
旨在解決的問題
由於 JavaScript 是單線程事件驅動的編程語言,通過回調函數管理多個任務。在快速迭代的開發中,因為回調函數的濫用,很容易產生被人所詬病的回調地獄問題。Promise
的非同步編程解決方案比回調函數更加合理,可讀性更強。
傳說中比較誇張的回調:
現實業務中依賴關係比較強的回調:
# 回調函數
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
梳理流程後:
# 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)
})
}
1.2 實現一個超簡易版的 Promise
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
怎麼用
2.1 使用 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)
2.2 鏈式調用
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
})
}
2.3 管理多個 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
2.4 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) }
2.5 錯誤處理
-
鏈式調用中儘量結尾跟
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
對象才行。
2.6 取消一個 Promise
當執行一個超級久的非同步請求時,若超過了能夠忍受的最大時長,往往需要取消此次請求,但是 Promise
並沒有類似於 cancel()
的取消方法,想結束一個 Promise
只能通過 resolve
或 reject
來改變其狀態,社區已經有了滿足此需求的開源庫 Speculation。
或者利用 Promise.race()
的機制來同時註入一個會超時的非同步函數,但是 Promise.race()
結束後主程式其實還在 pending
中,占用的資源並沒有釋放。
Promise.race([anAsyncFn(), timeout(5000)])
2.7 迭代器的應用
若想按順序執行一堆非同步程式,可使用 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
orrejected
)。- 使用
async
/await
時要考慮上下文的依賴性,避免造成不必要的阻塞。
更多文章訪問我的博客