原文连接 Medium - Master the JavaScript Interview: What is a Promise?javascript
一个promise
指的是一个可能会在将来的某个时间点产生一个单一值的对象:不管是一个 resolved 值,仍是一个未 resolved 值的缘由(好比发生了网络错误)。一个promise
可能为fulfilled
、 rejected
或 pending
三种状态中的一种。promise
用户可使用回调函数来处理fulfilled
和 rejected
状态。java
Promise
能够说是至关热心了,其构造器一旦被调用,promise
就会当即开始作你给它的任何任务。git
早期对promise
和futures
(两个概念相似/相关)的实现始于如 MultiLisp
和Concurrent Prolog
语言于 20 世纪 80年代早期的出现。promise
一词的使用是由 Barbara Liskov 和 Liuba Shrira 在 1988 年创造出来的1。github
我第一次在 JavaScript 中知道promise
这个概念时,Node
才刚刚出现,当时的社区也在积极的讨论实现异步行为的最佳方式。在一段时间里,社区使用过promises
这个概念,但最终落实在了标准Node
错误处理回调上。promise
几乎在同一时期,Dojo
框架中经过Deferred API
添加了promises
。随着公众对此持续不断兴趣和活跃度的高涨,最终造成了一个新的promise/A
规范使得多种promises
的实现得以统一。网络
jQuery 的异步行为围绕 promises 被重构。jQuery 对 promise 的支持与 Dojo 的 Deferred
极其相似,也很快因其大规模受众而成为 JavaScript 中最受欢迎的 promise 实现方式。然而,jQuery 不支持fulfilled/rejected
两个通道的链式调用行为和异常处理,而这些特性也是用户赖以使用 promise 构建应用的基础。app
尽管 jQuery 存在上述缺点,其依然成为当时 JavaScript promises 的主流实现,受喜好程度遥遥领先于像是Q
、When
或者Bluebird
这样的 promise 库。jQuery 实现的不兼容性催生了一些对 promise 的补充说明,从而造成了Promises/A+
规范。框架
ES6 中的 Promise 带来了对 Promises/A+ 规范的彻底兼容,另外还有一些很是重要的 API 也基于新的标准 Promise 而获得支持:最多见的有WHATWG Fetch
规范和异步函数标准。异步
这里说明了 promises 是符合 Promises/A+ 规范的,也是 ECMAScript 标准Promise
的实现。函数
promise 是一个能够从一个异步函数中返回的异步对象,它能够处于如下三种状态中的一种:
onFulfilled()
会被调用(好比resolve()
被调用)onRejected()
会被调用(好比reject()
被调用)fulfilled
或rejected
promise 的状态只要不是 pending 即表明其已肯定状态(resolved 或 rejected),有时人们会用 resolved 和 settled 来表示同一个意思:非 pending状态。
状态一旦肯定,promise 的状态就不能再被改变,调用 resolve()
或reject()
也不会产生任何影响。一个已肯定状态的 promise 的不可变性是其一大重要特性。
原生 JS promise 不对外暴露状态。实际上你可能更但愿把它当作一个黑盒机制来看待。只有当某函数的做用是建立一个 promise 时、或者是去访问 resolve 或 reject 时咱们才须要深刻 promise 的状态。
下面的函数会在指定时间后 resolve,而后返回一个 promise:
const wait = time => new Promise((resolve) => setTimeout(resolve, reject))
wait(3000).then(() => console.log('Hello!'));
复制代码
这里调用wait(3000)
会在等待 3000ms 后打印出 Hello!
。全部符合标准的 promises 都会定义一个.then()
方法,可向该方法中传递一个句柄,从而拿到 resolve 或 reject 的值。
ES6 的 promise 构造函数接收一个函数做为参数。该函数接收两个参数分别为resolve()
和reject()
。在上面的例子中,咱们只用到了resolve()
,而后调用了setTimeout()
建立一个延迟函数,最后在延迟函数执行完后调用resolve()
。
你能够选择只传给resolve()
或reject()
一个值,值会被传递到.then()
中的回调函数中。
每当我向reject()
中传一个值时,我都会传一个 Error 对象进去。通常来讲我指望两种解决状态:正常的圆满结局,或者是抛出一个异常。传一个 Error 对象进去会使得结果更明朗。
promises 的标准已经由Promises/A+ 规范社区定义好了。现存的不少种实现都遵照该规范,这其中就包括 JavaScript 标准 ECMAScript promises。
遵循上述规范的 promises 必须包含如下几点规则:
.then()
方法;每一个 promise 必须提供一个具有以下特性的.then()
方法:
promise.then(
onFulfilled?: Function,
onRejected?: Function
) => Promise
复制代码
.then()
方法必须符合下面的规则:
onFulfilled()
和onRejected()
皆为可选参数;onFulfilled()
会在 promise 的状态变为 fulfilled 时调用,promise 返回的值会被做为第一个参数;onRejected()
会在 promise 的状态变为 rejected 时调用,被拒绝的缘由会被做为第一个参数。缘由可能会是任何有效的 JavaScript 值,可是因为被拒绝基本上等同于抛出异常,因此我建议使用 Error 对象;onFulfilled()
和onRejected()
都不会被屡次调用;.then()
可能会在同一个 promise 上被调用屡次。换句话说,promise 可被用来合并回调函数;.then()
必须返回一个新的 promise,可称之为promise2
;onFulfilled()
或onRejected()
返回一个值为x
,x
是一个 promise,promise2
将用x
锁定。不然,promise2
会被值x
fulfilled。onFulfilled()
或onRejected()
抛出一个异常e
,promise2
必须以e
做为缘由被 rejected;onFulfilled()
不是函数,promise1
被 fulfilled,那么promise2
必须以相同的值被 fulfilled;onRejected()
不是函数,promise1
被 rejected,那么promise2
必须以相同的缘由被 rejected;因为.then()
老是返回一个新的 promise,这样就能够实现对链式 promise 中的错误进行精确控制。Promises 容许咱们模仿正常的同步代码行为(如 try...catch)。
就像同步代码同样,链式调用能够产生顺序执行的效果。好比下面这样:
fetch(url)
.then(process)
.then(save)
.catch(handleErrors)
;
复制代码
假设上面的fetch()
、process()
、save()
都返回 promises,process()
会等待fetch()
执行完毕后再开始执行,同理save()
也要等待process()
执行完毕才开始执行,handleErrors()
当且仅当前面的任何一个 promises 运行出错才会执行。
下面给出一个复杂的例子:
const wait = time => new Promise(
res => setTimeout(() => res(), time)
);
wait(200)
// onFulfilled() 能够返回一个新的 promise, `x`
.then(() => new Promise(res => res('foo')))
// 下一个 promise 会假设 `x`的状态
.then(a => a)
// 上面咱们返回了未被包裹的`x`的值
// 所以上面的`.then()`返回了一个 fulfilled promise
// 有了上面的值以后:
.then(b => console.log(b)) // 'foo'
// 须要注意的是 `null` 是一个有效的 promise 返回值:
.then(() => null)
.then(c => console.log(c)) // null
// 至此还未报错:
.then(() => {throw new Error('foo');})
// 相反, 返回的 promise 是 rejected
// error 的缘由以下:
.then(
// 因为上面的 error致使在这里啥都没打印:
d => console.log(`d: ${ d }`),
// 如今咱们处理这个 error (rejection 的缘由)
e => console.log(e)) // [Error: foo]
// 有了以前的异常处理, 咱们能够继续:
.then(f => console.log(`f: ${ f }`)) // f: undefined
// 下面的代码未打印任何东西. e 已经被处理过了,
// 因此该句柄并未被调用:
.catch(e => console.log(e))
.then(() => { throw new Error('bar'); })
// 当一个 promise 被 rejected, success 句柄就被跳过.
// 这里由于 'bar' 异常而不打印任何东西:
.then(g => console.log(`g: ${ g }`))
.catch(h => console.log(h)) // [Error: bar]
;
复制代码
须要注意的是 promise 同时具备成功和失败的句柄,因此下面代码的写法很常见:
save().then(
handleSuccess,
handleError
)
复制代码
可是若是 handleSuccess()
出错了怎么办?从.then()
中返回的 promise 就会被 rejected,可是后续就没有能捕获该错误信息的函数了 —— 意思就是你 app 中的一个错误被吞掉了,这可有点儿糟糕。
针对上述缘由,有人就将上面的代码称为一种反模式(anti-pattern),并建议使用以下写法替代:
save()
.then(handleSuccess)
.catch(handleError);
复制代码
其中的差别很微妙,但却很重要。在头一个例子中,来自save()
中的错误会被捕获,可是来自handleSuccess()
中的错误就会被吞掉。
.catch()
会处理来自不管是
save()
仍是
handleSuccess()
中的错误。
save()
的错误还有多是网络错误,而
handleSuccess()
中的错误可能来自于开发者忘记处理一个错误的状态码,要是你想对这两种错误进行不一样的处理该怎么办?那就能够选择下面这种处理方式了:
save()
.then(
handleSuccess,
handleNetworkError
)
.catch(handleProgrammerError)
复制代码
不管你倾向于哪一种方式,我都推荐你在全部的 promises 后面带上 .catch()
。
刚学会使用 promise 的用户老是有不少疑问,其中最多的就是关于如何取消/中断一个 promise。思路是这样的:直接去 reject 想要取消/中断的 promise,缘由就写「Cancelled」便可。但若是你要将它与常规错误处理方式区分开来的话,那就去开发本身的错误处理分支。
下面列出了几种人们在写取消/中断 promise 时常犯的错误:
.cancel()
添加.cancel()
使得 promise 非标准化了,同时也违背了 promise 的另外一个规定:只有建立了 promise 的函数才有能力去 resolve、reject 或 取消/中断该 promise。传播这种写法只会破坏函数的封装特性,怂恿人们在不恰当的地方操做 promise 代码,破坏了 promise。
有些聪明的人搞清楚了使用promise.race()
的方式来取消/中断 promise。这种方式的问题在于中断控制的操做是由建立该 promise 的函数发起的,这也是惟一一处恰当的进行清理动做的位置,好比说清理定时器或者经过解除对数据的引用来释放内存等等。
你知道当你忘记处理一个 promise 的拒绝状态时 Chrome 抛出的满控制台的警告信息吗?
通常来讲,我会在一个 promise 建立时就把 promise 全部须要的信息都传给它,以便 promise 决定如何进行 resolve/reject/cancel。这种方式并不须要一个 .cancel()
方法附着在 promise 上。你可能想知道的是怎么才能知道是否要在 promise 建立时知道它将要被取消。
咱们要传的那个决定是否要取消的值能够是 promise 本身,看起来可能像下面这样:
const wait = (
time,
cancel = Promise.reject()
) => new Promise((resolve, reject) => {
const timer = setTimeout(resolve, time);
const noop = () => {};
cancel.then(() => {
clearTimeout(timer);
reject(new Error('Cancelled'));
}, noop);
});
const shouldCancel = Promise.resolve(); // Yes, cancel
// const shouldCancel = Promise.reject(); // No cancel
wait(2000, shouldCancel).then(
() => console.log('Hello!'),
(e) => console.log(e) // [Error: Cancelled]
);
复制代码
这里使用了默认分配的参数告诉它默认是不取消的。这样使得cancel
参数是可选的。而后咱们设置一个定时器,这里咱们拿到计时器的 ID 以便于后面取消它。
咱们使用cancel.then()
来处理取消/中断和资源的清理。它的运行条件是在 resolve 以前让 promise 取消。若是你取消的过晚,你就错过了取消的时机。
你可能比较好奇noop
函数的做用是啥,noop 一词表示空操做,意指啥都不作。要是不指定这个函数,V8 引擎会抛出警告:UnhandledPromiseRejectionWarning: Unhandled promise rejection
,因此老是记得去处理 promise 的 rejection 是个好习惯,即便你的句柄为 noop。
上面的wait()
计时器固然是极好的,但咱们要继续将上面这种思路作进一步的抽象,来封装全部你须要知道的东西:
onCancel
的清理操做自己也有可能抛异常,该异常也须要处理。让咱们来建立一个可中断的 promise 工具函数吧,这样你就能够用来包裹任何 promise 了。形式以下:
speculation(fn: SpecFunction, shouldCancel: Promise) => Promise
复制代码
SpecFunction
就像你传入 Promise 构造器中的函数同样,惟一的不一样在于它有一个onCancel
句柄:
SpecFunction(resolve: Function, reject: Function, onCancel: Function) => Void
复制代码
const speculation = (
fn,
cancel = Promise.reject()
) => new Promise((resolve, reject) => {
const noop = () => {};
const onCancel = (
handleCancel
) => cancel.then(
handleCancel,
noop
)
.catch(e => reject(e))
;
fn(resolve, reject, onCancel);
});
复制代码
上例只是其做用要旨,其实还有须要边界状况须要你去考虑。我本身写了一个完整的版本供你们参考,speculation
文章太长,翻译到后半段着实翻译不下去了,主要仍是自身对 promise 的理解还不够深,后面就看不懂了,但仍是以为要善始善终,把这件事作完,后面懂了再回头完善,never giveup!
[1] Barbara Liskov; Liuba Shrira (1988). “Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems”. Proceedings of the SIGPLAN ’88 Conference on Programming Language Design and Implementation; Atlanta, Georgia, United States, pp. 260–267. ISBN 0–89791–269–1, published by ACM. Also published in ACM SIGPLAN Notices, Volume 23, Issue 7, July 1988.