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
時要考慮上下文的依賴性,避免形成沒必要要的阻塞。更多文章訪問個人博客