JavaScript Promise 的理解和使用
一、什么是 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。
1.1.1 旨在解决的问题
由于 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
2.4.1 使用 async
/ await
的优势
2.4.1.1 简洁干净
写更少的代码,不需要特地创建一个匿名函数,放入 then()
方法中等待一个响应。
- // Promise
- function getUserInfo () {
- return getData().then(
- data => {
- return data
- }
- )
- }
-
- // async / await
- async function getUserInfo () {
- return await getData()
- }
2.4.1.2 处理条件语句
当一个异步返回值是另一段逻辑的判断条件,链式调用将随着层级的叠加变得更加复杂,让人很容易在代码中迷失自我。使用 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 {
- // ...
- }
- }
2.4.1.3 处理中间值
异步函数常常存在一些异步返回值,作用仅限于成为下一段逻辑的入场券,如果经历层层链式调用,很容易成为另一种形式的“回调地狱”。
- // 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)
- // ...
- }
对于多个异步返回中间值,搭配 Promise.all
使用能够提升逻辑性和性能。
- // async / await & Promise.all
- async function foo() {
- // ...
- const [ a, b, c ] = await Promise.all([ promiseFnA(), promiseFnB(), promiseFnC() ])
- const d = await promiseFnD()
- // ...
- }
2.4.1.4 靠谱的 await
await 'str'
等于 await Promise.resolve('str')
,await
会把任何不是 Promise
的值包装成 Promise
,看起来貌似没有什么用,但是在处理第三方接口的时候可以 “Hold” 住同步和异步返回值,否则对一个非 Promise
返回值使用 then()
链式调用则会报错。
2.4.2 使用 async
/ await
的缺点
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 错误处理
1. 链式调用中尽量结尾跟 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)
2. 通过全局属性监听未被处理的 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
时要考虑上下文的依赖性,避免造成不必要的阻塞。
版权声明
本博客所有的原创文章,作者皆保留版权。转载必须包含本声明,保持本文完整,并以超链接形式注明作者后除和本文原始地址:https://blog.mazey.net/understand-promise
(完)