一、什么是 Promise

1.1 Promise 的前世今生

Promise 最早出现在 1988 年,由 Barbara LiskovLiuba Shrira 首创(论文:Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems)。并且在语言 MultiLispConcurrent Prolog 中已经有了类似的实现。

JavaScript 中,Promise 的流行是得益于 jQuery 的方法 jQuery.Deferred(),其他也有一些更精简独立的 Promise 库,例如:QWhenBluebird

#?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,必须要遵循如下规则:

  1. Promise 是一个提供符合标准的 then() 方法的对象。
  2. 初始状态是 pending,能够转换成 fulfilled 或 rejected 状态。
  3. 一旦 fulfilled 或 rejected 状态确定,再也不能转换成其他状态。
  4. 一旦状态确定,必须要返回一个值,并且这个值是不可修改的。

讲讲 Promise_Promise

ECMAScript's Promise global is just one of many Promises/A+ implementations.

主流语言对于 Promise 的实现:Golang/go-promisePython/promiseC#/Real-Serious-Games/c-sharp-promisePHP/Guzzle PromisesJava/IOUObjective-C/PromiseKitSwift/FutureLibPerl/stevan/promises-perl

旨在解决的问题

由于 JavaScript 是单线程事件驱动的编程语言,通过回调函数管理多个任务。在快速迭代的开发中,因为回调函数的滥用,很容易产生被人所诟病的回调地狱问题。Promise 的异步编程解决方案比回调函数更加合理,可读性更强。

传说中比较夸张的回调:

讲讲 Promise_Promise_02

现实业务中依赖关系比较强的回调:

#?回调函数
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

讲讲 Promise_Promise_03

#?实现
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 出现之前往往使用回调函数管理一些异步程序的状态。

讲讲 Promise_Promise_04

#?常见的异步?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 的优势:

  1. 简洁干净

    写更少的代码,不需要特地创建一个匿名函数,放入 then() 方法中等待一个响应。

    #?Promise
    function?getUserInfo?()?{
    ????return?getData().then(
    ????????data?=>?{
    ????????????return?data
    ????????}
    ????)
    }
    
    #?async?/?await
    async?function?getUserInfo?()?{
    ????return?await?getData()
    }
  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?{
    ????????//?...
    ????}
    }
  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)
    ????//?...
    }
  4. 靠谱的 await

    await 'qtt' 等于 await Promise.resolve('qtt'),await 会把任何不是 Promise 的值包装成 Promise,看起来貌似没有什么用,但是在处理第三方接口的时候可以 “Hold” 住同步和异步返回值,否则对一个非 Promise 返回值使用 then() 链式调用则会报错。

使用 async / await 的缺点:

  1. async 永远返回 Promise 对象,不够灵活,很多时候我只想单纯返回一个基本类型值。

  2. 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)的拒绝状态监听事件:

    //?初始化列表
    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)
  • unhandledrejection 当 Promise 被拒绝,并且没有提供拒绝处理程序时,触发该事件。
  • rejectionhandled 当 Promise 被拒绝时,若拒绝处理程序被调用,触发该事件。

注意: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)

三、总结

  1. 每当要使用异步代码时,请考虑使用 Promise。
  2. Promise 中所有方法的返回类型都是 Promise。
  3. Promise 中的状态改变是一次性的,建议在 reject() 方法中传递 Error 对象。
  4. 确保为所有的 Promise 添加 then() 和 catch() 方法。
  5. 使用 Promise.all() 行运行多个 Promise。
  6. 倘若想在 then() 或 catch() 后都做点什么,可使用 finally()。
  7. 可以将多个 then() 挂载在同一个 Promise 上。
  8. async (异步)函数返回一个 Promise,所有返回 Promise 的函数也可以被视作一个异步函数。
  9. await 用于调用异步函数,直到其状态改变(fulfilled or rejected)。
  10. 使用 async / await 时要考虑上下文的依赖性,避免造成不必要的阻塞。