一、什么是 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 =https://www.cnblogs.com/mazey/p/> {
// 渲染游戲串列
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 =https://www.cnblogs.com/mazey/p/> {
// 渲染游戲串列
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 = https://www.cnblogs.com/mazey/p/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 =https://www.cnblogs.com/mazey/p/> {
// ...
// 經歷了一系列資料處理
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 =https://www.cnblogs.com/mazey/p/> { return data } ) } # async / await async function getUserInfo () { return await getData() }條件陳述句
當一個異步回傳值是另一段邏輯的判斷條件,鏈式呼叫將隨著層級的疊加變得更加復雜,讓人很容易在代碼中迷失自我,使用
async/await將使代碼可讀性變得更好,# Promise function getGameInfo () { getUserAbValue().then( abValue =https://www.cnblogs.com/mazey/p/> { 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 =https://www.cnblogs.com/mazey/p/> { // ... } ) } ) } ) } # async / await async function getGameInfo() { const token = await getToken() const level = await getLevel(token) const data = await getInfo(token, level) // ... }靠譜的
awaitawait '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用于呼叫異步函式,直到其狀態改變(fulfilledorrejected),- 使用
async/await時要考慮背景關系的依賴性,避免造成不必要的阻塞,
更多文章訪問我的博客
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/47807.html
標籤:JavaScript
