新聞中心
1、什么是 Promise

為普寧等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計(jì)制作服務(wù),及普寧網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務(wù)為網(wǎng)站制作、成都網(wǎng)站設(shè)計(jì)、普寧網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會(huì)得到認(rèn)可,從而選擇與我們長期合作。這樣,我們也可以走得更遠(yuǎn)!
1.1 Promise 的背景介紹
Promise 最早出現(xiàn)在 1988 年,由 Barbara Liskov、Liuba Shrira 首創(chuàng)(論文:Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems[1])。并且在語言 MultiLisp 和 Concurrent Prolog 中已經(jīng)有了類似的實(shí)現(xiàn)。
JavaScript 中,Promise 的流行是得益于 jQuery 的方法 jQuery.Deferred(),其他也有一些更精簡獨(dú)立的 Promise 庫,例如:Q、When、Bluebird。
// Q/2009-2017
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 并沒有嚴(yán)格按照規(guī)范來制定接口,促使了官方對 Promise 的實(shí)現(xiàn)標(biāo)準(zhǔn)進(jìn)行了一系列重要的澄清,該實(shí)現(xiàn)規(guī)范被命名為 Promise/A+。后來 ES6(也叫 ES2015,2015 年 6 月正式發(fā)布)也在 Promise/A+ 的標(biāo)準(zhǔn)上官方實(shí)現(xiàn)了一個(gè) Promise 接口。
new Promise( function(resolve, reject) {...} /* 執(zhí)行器 */ )想要實(shí)現(xiàn)一個(gè) Promise,必須要遵循如下規(guī)則:
Promise 是一個(gè)提供符合標(biāo)準(zhǔn)的 then() 方法的對象。
初始狀態(tài)是 pending,能夠轉(zhuǎn)換成 fulfilled 或 rejected 狀態(tài)。
一旦 fulfilled 或 rejected 狀態(tài)確定,再也不能轉(zhuǎn)換成其他狀態(tài)。
一旦狀態(tài)確定,必須要返回一個(gè)值,并且這個(gè)值是不可修改的。
ECMAScript’s Promise global is just one of many Promises/A+ implementations.
主流語言對于 Promise 的實(shí)現(xiàn):Golang/promise、Python/promise、C#/Real-Serious-Games/c-sharp-promise、PHP/Guzzle Promises、Java/IOU、Objective-C/PromiseKit、Swift/FutureLib、Perl/stevan/promises-perl。
// Golang Example
func main() {
p1 := promise.New(func(resolve func(int), reject func(error)) {
factorial := findFactorial(20)
resolve(factorial)
})
p2 := promise.New(func(resolve func(string), reject func(error)) {
ip, err := fetchIP()
if err != nil {
reject(err)
return
}
resolve(ip)
})
factorial, _ := p1.Await()
fmt.Println(factorial)
IP, _ := p2.Await()
fmt.Println(IP)
}
// Other Code...
1.1.1 旨在解決的問題
由于 JavaScript 是單線程事件驅(qū)動(dòng)的編程語言,通過回調(diào)函數(shù)管理多個(gè)任務(wù)。在快速迭代的開發(fā)中,因?yàn)榛卣{(diào)函數(shù)的濫用,很容易產(chǎn)生被人所詬病的回調(diào)地獄問題。Promise 的異步編程解決方案比回調(diào)函數(shù)更加合理,可讀性更強(qiáng)。
傳說中比較夸張的回調(diào):
現(xiàn)實(shí)業(yè)務(wù)中依賴關(guān)系比較強(qiáng)的回調(diào):
// 回調(diào)函數(shù)
function renderPage() {
const secret = genSecret()
// 獲取用戶登錄態(tài)
getUserToken({
secret,
success: token => {
// 獲取游戲列表
getGameList({
token,
success: data => {
// 渲染游戲列表
render({
list: data.list,
success: () => {
// 埋點(diǎn)數(shù)據(jù)上報(bào)
report()
},
fail: err => {
console.error(err)
}
})
},
fail: err => {
console.error(err)
}
})
},
fail: err => {
console.error(err)
}
})
}
實(shí)際上更真實(shí)的情況,往往是一個(gè)回調(diào)函數(shù)在多個(gè)文件間透傳,要搞清楚最終在哪里觸發(fā)需要翻越整個(gè)項(xiàng)目。
使用 Promise 梳理流程后:
// Promise
function renderPage() {
const secret = genSecret()
// 獲取用戶登錄態(tài)
getUserToken(token)
.then(token => {
// 獲取游戲列表
return getGameList(token)
})
.then(data => {
// 渲染游戲列表
return render(data.list)
})
.then(() => {
// 埋點(diǎn)數(shù)據(jù)上報(bào)
report()
})
.catch(err => {
console.error(err)
})
}
若其中某個(gè)流程需要復(fù)用,單獨(dú)把它抽離出來即可。
// 獲取游戲列表
// 僅為示例,與實(shí)際業(yè)務(wù)無關(guān)
function getGameXYZ() {
const secret = genSecret()
// 獲取用戶登錄態(tài)
return getUserToken(token)
.then(token => {
// 獲取游戲列表
return getGameList(token)
})
}
// 渲染頁面
function renderPage() {
getGameXYZ()
.then(data => {
// 渲染游戲列表
return render(data.list)
})
.then(() => {
// 埋點(diǎn)數(shù)據(jù)上報(bào)
report()
})
.catch(err => {
console.error(err)
})
}
// 其他場景
function doABC() {
getGameXYZ()
.then(data => {
// ...
})
.then(data => {
// ...
})
.catch(err => {
console.error(err)
})
}
1.2 實(shí)現(xiàn)一個(gè)超簡易版的 Promise
Promise 的運(yùn)轉(zhuǎn)實(shí)際上是一個(gè)觀察者模式,then() 中的匿名函數(shù)充當(dāng)觀察者,Promise 實(shí)例充當(dāng)被觀察者。
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
// 實(shí)現(xiàn)
const defer = () => {
let pending = [] // 充當(dāng)狀態(tài)并收集觀察者
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!
2、Promise 怎么用
2.1 使用 Promise 異步編程
在 Promise 出現(xiàn)之前往往使用回調(diào)函數(shù)管理一些異步程序的狀態(tài)。
// 常見的異步 Ajax 請求格式
ajax(url, successCallback, errorCallback)
Promise 出現(xiàn)后使用 then() 接收事件的狀態(tài),且只會(huì)接收一次。
案例:插件初始化
工作中使用封裝好的插件時(shí),往往需要等待插件初始化成功后進(jìn)行下一步操作。
使用回調(diào)函數(shù):
插件代碼:
const PPlugin = {/* Pass */ }
let initOk = null
const ppInitStatus = new Promise(resolve => initOk = resolve)
PPlugin.init = callback => {
ppInitStatus.then(callback).catch(console.error)
}
// 客戶端橋接...
// 服務(wù)端接口...
// 經(jīng)歷了一系列同步異步程序后初始化完成
initOk(/* 數(shù)據(jù) */)相對于使用回調(diào)函數(shù),邏輯更清晰,什么時(shí)候初始化完成和觸發(fā)回調(diào)一目了然,不再需要重復(fù)判斷狀態(tài)和回調(diào)函數(shù)。當(dāng)然更好的做法是只給使用方輸出狀態(tài)和數(shù)據(jù),至于如何使用由使用方?jīng)Q定。
插件代碼:
const PPlugin = {/* Pass */ }
let initOk = null
PPlugin.init = new Promise(resolve => initOk = resolve)
// 客戶端橋接...
// 服務(wù)端接口...
// 經(jīng)歷了一系列同步異步程序后初始化完成
initOk(/* 數(shù)據(jù) */)使用插件:
2.2 鏈?zhǔn)秸{(diào)用
then() 必然返回一個(gè) Promise 對象,Promise 對象又擁有一個(gè) then() 方法,這正是 Promise 能夠鏈?zhǔn)秸{(diào)用的原因。
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) // 由此可見,每次返回的是實(shí)例后面跟的最后一個(gè) 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 '最強(qiáng)王者'
})
p.then(console.log.bind(null, '是誰活到了最后:')) // 是誰活到了最后: 最強(qiáng)王者
由于返回一個(gè) Promise 結(jié)構(gòu)體永遠(yuǎn)返回的是鏈?zhǔn)秸{(diào)用的最后一個(gè) then(),所以在處理封裝好的 Promise 接口時(shí)沒必要在外面再包一層 Promise。
// 包一層 Promise
function api() {
return new Promise((resolve, reject) => {
axios.get(/* 鏈接 */).then(data => {
// ...
// 經(jīng)歷了一系列數(shù)據(jù)處理
resolve(data.xxx)
})
})
}
// 更好的做法:利用鏈?zhǔn)秸{(diào)用
function api() {
return axios.get(/* 鏈接 */).then(data => {
// ...
// 經(jīng)歷了一系列數(shù)據(jù)處理
return data.xxx
})
}
2.3 管理多個(gè) Promise 實(shí)例
Promise.all() / Promise.race() 可以將多個(gè) Promise 實(shí)例包裝成一個(gè) Promise 實(shí)例,在處理并行的、沒有依賴關(guān)系的請求時(shí),能夠節(jié)約大量的時(shí)間。
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 實(shí)際上只是建立在 Promise 之上的語法糖,讓異步代碼看上去更像同步代碼,所以 async&await 在 JavaScript 線程中是非阻塞的,但在當(dāng)前函數(shù)作用域內(nèi)具備阻塞性質(zhì)。
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
2.4.1 使用 async&await 的優(yōu)勢
簡潔干凈
寫更少的代碼,不需要特地創(chuàng)建一個(gè)匿名函數(shù),放入 then() 方法中等待一個(gè)響應(yīng)。
// Promise
function getUserInfo() {
return getData().then(
data => {
return data
}
)
}
// async / await
async function getUserInfo() {
return await getData()
}
處理?xiàng)l件語句
當(dāng)一個(gè)異步返回值是另一段邏輯的判斷條件,鏈?zhǔn)秸{(diào)用將隨著層級(jí)的疊加變得更加復(fù)雜,很容易讓人混淆。使用 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 {
// ...
}
}
處理中間值
異步函數(shù)常常存在一些異步返回值,作用僅限于成為下一段邏輯的入場券,如果經(jīng)歷層層鏈?zhǔn)秸{(diào)用,很容易成為另一種形式的“回調(diào)地獄”。
// 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)
// ...
}
對于多個(gè)異步返回中間值,搭配 Promise.all 使用能夠提升邏輯性和性能。
// async / await & Promise.all
async function foo() {
// ...
const [a, b, c] = await Promise.all([promiseFnA(), promiseFnB(), promiseFnC()])
const d = await promiseFnD()
// ...
}
靠譜的 await
await 'str' 等于 await Promise.resolve('str'),await 會(huì)把任何不是 Promise 的值包裝成 Promise,看起來貌似沒有什么用,但是在處理第三方接口的時(shí)候可以 “Hold” 住同步和異步返回值,否則對一個(gè)非 Promise 返回值使用 then() 鏈?zhǔn)秸{(diào)用則會(huì)報(bào)錯(cuò)。
2.4.2 避免濫用 async&await
await 阻塞 async 函數(shù)中的代碼執(zhí)行,在上下文關(guān)聯(lián)性不強(qiáng)的代碼中略顯累贅。
// async / await
async function initGame() {
render(await getGame()) // 等待獲取游戲執(zhí)行完畢再去獲取用戶信息
report(await getUserInfo())
}
// Promise
function initGame() {
getGame()
.then(render)
.catch(console.error)
getUserInfo() // 獲取用戶信息和獲取游戲同步進(jìn)行
.then(report)
.catch(console.error)
}
2.4.3 ES2021 新特性:Top-level await
Node.js 14+ 版本后,可以在 JavaScript module 中使用 await 操作符。在這之前,只能通過在 async 聲明的場景中使用 await 操作符。
MDN 官方案例[2]:
// fetch request
const colors = fetch('../data/colors.json')
.then((response) => response.json())
export default await colors
2.5 錯(cuò)誤處理
1. 鏈?zhǔn)秸{(diào)用中盡量結(jié)尾跟 catch 捕獲錯(cuò)誤,而不是第二個(gè)匿名函數(shù)。因?yàn)橐?guī)范里注明了若 then() 方法里面的參數(shù)不是函數(shù)則什么都不做,所以 catch(rejectionFn) 其實(shí)就是 then(null, rejectionFn) 的別名。
anAsyncFn().then(
resolveSuccess, // 無法捕獲
rejectError // `rejectError` 捕獲 `anAsyncFn`
)
↑在以上代碼中,anAsyncFn() 拋出來的錯(cuò)誤 rejectError 會(huì)正常接住,但是 resolveSuccess 拋出來的錯(cuò)誤將無法捕獲,所以更好的做法是永遠(yuǎn)使用 catch。
anAsyncFn()
.then(resolveSuccess)
.catch(rejectError) // 盡量使用 `catch`
若想錯(cuò)誤管理精細(xì)一點(diǎn),也可以通過 rejectError 來捕獲 anAsyncFn() 的錯(cuò)誤,catch 捕獲 resolveSuccess 的錯(cuò)誤。
anAsyncFn()
.then(
resolveSuccess,
rejectError // 捕獲 `anAsyncFn()`
)
.catch(handleError) // 捕獲 `resolveSuccess`
2. 通過全局屬性監(jiān)聽未被處理的 Promise 錯(cuò)誤。
瀏覽器環(huán)境(window)的拒絕狀態(tài)監(jiān)聽事件:
- unhandledrejection 當(dāng) Promise 被拒絕,并且沒有提供拒絕處理程序時(shí),觸發(fā)該事件。
- rejectionhandled 當(dāng) Promise 被拒絕時(shí),若拒絕處理程序被調(diào)用,觸發(fā)該事件。
// 初始化列表
const unhandledRejections = new Map()
// 監(jiān)聽未處理拒絕狀態(tài)
window.addEventListener('unhandledrejection', e => {
unhandledRejections.set(e.promise, e.reason)
})
// 監(jiān)聽已處理拒絕狀態(tài)
window.addEventListener('rejectionhandled', e => {
unhandledRejections.delete(e.promise)
})
// 循環(huán)處理拒絕狀態(tài)
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()) 這種方式不能直接觸發(fā) unhandledrejection 事件,必須是滿足已經(jīng)進(jìn)行了 then() 鏈?zhǔn)秸{(diào)用的 Promise 對象才行。
2.6 取消一個(gè) Promise
當(dāng)執(zhí)行一個(gè)超級(jí)久的異步請求時(shí),若超過了能夠忍受的最大時(shí)長,往往需要取消此次請求,但是 Promise 并沒有類似于 cancel() 的取消方法,想結(jié)束一個(gè) Promise 只能通過 resolve 或 reject 來改變其狀態(tài),社區(qū)已經(jīng)有了滿足此需求的開源庫 Speculation。
或者利用 Promise.race() 的機(jī)制來同時(shí)注入一個(gè)會(huì)超時(shí)的異步函數(shù),但是 Promise.race() 結(jié)束后主程序其實(shí)還在 pending 中,占用的資源并沒有釋放。
Promise.race([anAsyncFn(), timeout(5000)])
2.7 迭代器的應(yīng)用
若想按順序執(zhí)行一堆異步程序,可使用 reduce。每次遍歷返回一個(gè) Promise 對象,在下一輪 await 住從而依次執(zhí)行。相同的場景,也可以使用遞歸實(shí)現(xiàn),但是在 JavaScript 中隨著數(shù)量增加,超出調(diào)用棧最大次數(shù),便會(huì)報(bào)錯(cuò)。
function wasteTime(ms) {
return new Promise(resolve => setTimeout(() => {
resolve(ms)
console.log('waste', ms)
}, ms))
}
// 依次浪費(fèi) 3 4 5 3 秒 === 15 秒
const arr = [3000, 4000, 5000, 3000]
arr.reduce(async (last, curr) => {
await last
return wasteTime(curr)
}, undefined)3、游戲聯(lián)運(yùn)業(yè)務(wù)中的實(shí)踐
3.1 登錄流程優(yōu)化
游戲聯(lián)運(yùn)業(yè)務(wù)中的登錄模塊,因?yàn)槭褂脠鼍暗膹?fù)雜性,會(huì)有一個(gè)回調(diào)函數(shù)在各個(gè)文件間(此處指 Login.vue 和 State.js)傳遞。若想知道這個(gè)回調(diào)函數(shù)在哪里觸發(fā)、傳遞了什么數(shù)據(jù),需要逐層查找邏輯,并且需要進(jìn)行類型判斷。
Login.vue
methods: {
comLogin() {
// Other Code...
State.quickLogin(callback) // 1
},
},State.js
quickLogin(callback) {
// Other Code...
if (this.canQuickLogin) {
this.ppQuickLogin(callback) // 2
} else {
getScript('//cdxwcx.com/ppquicklogin.min.js').then(() => {
// Other Code...
this.ppQuickLogin(callback) // 2
})
}
}
ppQuickLogin(callback) {
// Other Code...
this.getUserState(callback) // 3
}
getUserState(callback) {
// Other Code...
if (isFunction(callback)) {
callback(this.userInfo) // 4
}
}
使用 Promise 改寫后,簡單調(diào)用時(shí)僅需要關(guān)注狀態(tài)和值(userInfo),無需過度關(guān)注其在上游鏈路經(jīng)歷了什么。
Login.vue
methods: {
async comLogin() {
const userInfo = await State.quickLogin() // 1
// Other Code...
},
},State.js
async quickLogin() {
// Other Code...
if (this.canQuickLogin) {
return this.ppQuickLogin() // 2
} else {
return getScript('//cdxwcx.com/ppquicklogin.min.js').then(() => {
// Other Code...
return this.ppQuickLogin() // 2
})
}
}
async ppQuickLogin() {
// Other Code...
return this.getUserState() // 3
}
async getUserState() {
// Other Code...
return this.userInfo
}
3.2 游戲頁推送下載優(yōu)化
當(dāng)前業(yè)務(wù)背景下,PC 游戲詳情頁常常充當(dāng)手機(jī)游戲的宣傳頁面,來引導(dǎo)用戶在手機(jī)端 App 下載對應(yīng)游戲。在展示/進(jìn)行推送之前需要確認(rèn)幾個(gè)前置條件,使用回調(diào)函數(shù)往往會(huì)產(chǎn)生冗余代碼。
index.vue
methods: {
getSwitchStatus(callback) {
// Other Code...
if (isFunction(callback)) {
callback(isSwitchOn)
}
},
getLoginStatus(callback) {
// Other Code...
if (isFunction(callback)) {
callback(isLogined)
}
},
pushGame() {
// Other Code...
this.getSwitchStatus(
isSwitchOn => {
if (isSwitchOn) {
this.getLoginStatus(
isLogined => {
if (isLogined) {
// Other Code...
} else {
this.showLoginModal()
}
}
)
}
}
)
},
},使用 Promise 改寫后,業(yè)務(wù)邏輯上便會(huì)更加清晰一點(diǎn)。
index.vue
methods: {
async getSwitchStatus() {
// Other Code...
return isSwitchOn
},
async getLoginStatus() {
// Other Code...
return isLogined
},
async pushGame() {
// Other Code...
const [isSwitchOn, isLogined] = await Promise.all([this.getSwitchStatus(), this.getLoginStatus()])
if (isSwitchOn && isLogined) {
// Other Code...
} else {
// Other Code...
}
},
},4、總結(jié)
- 每當(dāng)要使用異步代碼時(shí),請考慮使用 Promise。
- Promise 中所有方法的返回類型都是 Promise。
- Promise 中的狀態(tài)改變是一次性的,建議在 reject() 方法中傳遞 Error 對象。
- 盡量為所有的Promise 添加 then() 和 catch() 方法。
- 使用Promise.all() 去運(yùn)行多個(gè) Promise。
- 倘若想在then() 或 catch() 后都做點(diǎn)什么,可使用 finally()。
- 可以將多個(gè) then() 掛載在同一個(gè) Promise 上。
- async (異步)函數(shù)返回一個(gè) Promise,所有返回 Promise 的函數(shù)也可以被視作一個(gè)異步函數(shù)。
- await 用于調(diào)用異步函數(shù),直到其狀態(tài)改變(fulfilled or rejected)。
- 使用async / await 時(shí)要考慮上下文的依賴性,避免造成不必要的阻塞。
參考鏈接:
[1]https://dl.acm.org/doi/10.1145/960116.54016
[2]https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#top_level_await
本期作者
錢程
游戲技術(shù)中臺(tái)資深開發(fā)工程師
網(wǎng)站標(biāo)題:Promise:異步編程的理解和使用
本文鏈接:http://www.dlmjj.cn/article/dhsjegs.html


咨詢
建站咨詢
