
“精通 JavaScript 面试” 是一个系列的文章,旨在帮助面试者准备他们在申请中高级职位时可能遇到的常见问题。这些是我在现实面试中经常提出的问题。
什么是 Promise?
Promise 是一个对象,在未来的某个时刻产生一个值:已完成 (resolved) 的值或者是未完成的原因(e.g. 网络错误)。Promise 可能是下面三种状态中的一种:fulfilled,rejected,pending 。Promise 提供回调来处理 fulfilled 返回的值或者 rejection 的原因。
Promise 会立即执行,这意味着一旦 promise 构造函数被调用,promise 就会开始执行你给它的任何任务。如果你需要 promise 懒加载,参考 observables 或者 tasks。
Promise 的不完全历史
早在1980年代,promise 和 futures (类似/相关的想法) 的早期实现开始出现在 MultiLisp 和 Concurrent Prolog 等语言中。“promise” 这个概念是由 Barbara Liskov 和 Liuba Shrira 在1998年提出。
我第一次听说 JavaScript 中的 promise,Node 才刚刚出现,Node 社区正在讨论异步行为的最佳方案。社区实验了一段时间 promise,但最终选择了 error-first 回调作为 Node 标准。
大概在同一时间,Dojo 通过 Deferred API 添加了 promises 。不断增长的兴趣和活跃度最终导致了新的 Promises/A 规范的形成,提高了不同 promises 之间的互操作性。
jQuery 的异步操作是围绕 promises 的重构。jQuery 的 promise 和 Dojo 的 Deferred 非常相似,并且由于 jQuery 的流行,它一度在 JavaScript 中被广泛使用。但是 it did not support the two channel (fulfilled/rejected) chaining behavior & exception management 人们期望在 promise 上的构建工具。
尽管有这些缺点,jQuery 让 JavaScript promise 成为主流,并且一下更好的独立的 promise 库,例如 Q , When , 和 Bluebird 变得非常流行。jQuery 的实现的不兼容性促使 promise 规范中的一些重要说明,它被重写并重命名为 Promises/A+ specification 。
ES6 使用了遵循 Promises/A+ 兼容的 Promise 作为全局变量,并且一些非常重要的 API 构建在新的 Promise 标准之上:特别是 WHATWG Fetch 规范和 Async Functions 标准(第3阶段草案)
本文描述的是与 Promises/A+ 规范兼容的 promise,着重于基于 ECMAScript 标准的 Promise 实现。
Promises 是如何工作的
promise 是一个从异步函数同步返回的对象。它有3种可能的状态:
Fulfilled: 将会调用
onFulfilled()(e.g., 调用resolve())Rejected: 将会调用
onRejected()(e.g., 调用reject())Pending: 非 fulfilled 或者 rejected
如果 promise 不在 pending 状态,它就固定了(处于 fulfilled 或 rejected)。有时我们用 resolved 或 settled 表示同一件事:not pending 。
promise 一旦被固定,就不能重新固定。调用 resolve() 或 reject() 将没有效果。被固定的 promise 的不可变性是一个重要的特性。
原生 JavaScript promise 不暴露 promise 状态。相反,你应该把 promise 视为一个黑盒。只有创建这个 promise 的函数才知道 promise 的状态,或者说有 resolve 或 reject 的权限。
这是一个返回一个 promise 的函数,并在制定的时间延迟后 resolve 。
调用 wait(3000) 会等待3000毫秒 (3秒) ,然后调用打印 'Hello!' 。所以与规范兼容的 promise 都定义了 .then() 方法,你可以传入一个函数来处理 resolve 或 reject 返回的值。
ES6 promise 的构造函数需要一个函数。这个函数需要两个参数,resovle() 和 reject() 。在上面的例子中,我们只使用了 resolve() ,将 reject() 从参数列表中删除了。我们调用 setTimeout() 来创建延迟,并在完成后调用 resolve() 。
你可以按需向 resolve() 或 reject() 中传值,这个值会作为参数传入 .then() 的回调函数。
当我调用 reject() 并且传入一个值,我总是传入一个 Error 对象。一般来说,我想要两种可能的解决状态:正常的或异常的 (阻碍正常状态) 。传入一个 Error 对象来表示。
Promise 的重要规则
标准的 Promise 是由 Promises/A+ specification 社区定义的。有很多符合标准的实现,包括 JavaScript ECMAScript promise。
Promise 必须遵循下面的规则:
Promise 或 “thenable” 需要提供标准的
.then()方法。pending 状态的 promise 可以转换为 fulfilled 或 rejected 状态。
fulfilled 或 rejected 状态的 promise 是固定的,不能转换为其他状态。
一旦 promise 固定,一定有一个值 (可以是 undefined) 。这个值不可变。
在这个上下午的变更指的是恒等 (===) 。fulfilled 的值可以是一个对象,这个对象的属性是可变的。
所以的 promise 都必须实现具有下面方法签名的 .then() 方法:
1 | promise.then( |
.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将会转换为 fulfilled 状态并且值为x。不论
onFulfilled或onRejected跑出一个异常e,promise2一定会 rejected 并且e作为原因。如果
onFulfilled不是一个函数并且promise1处于 fulfilled 状态,promise2一定是 fulfilled 状态并且和promise1有同样的值。如果
onRejected不是一个函数并且promise1处于 rejected 状态,promise2一定是 rejected 状态并且和promise1的原因相同。
Promise 链
因为 .then() 总是返回一个新的 promise ,所以可以链接 promises 并精确控制错误的处理方式和位置。Promises 允许你模仿同步代码的 try/catch 方式。
链式调用会形成一个有序的序列,类似于同步代码。换句话说,你可以这么做:
1 | fetch(url) |
假设 fetch() process() save() 每个函数都返回 promises , fetch() 完成后 process() 才开始执行, process() 完成后 save() 才开始执行。任何一个 promise reject 后 handleErrors() 才会执行。
这是一个复杂的 promise 链 的例子,并具有多个 rejection:
Error 处理
注意 promise 同时有成功和错误的 handler ,下面的代码很常见:
1 | save().then( |
但是 handleSuccess() 抛出错误应该怎么做?从 .then() 返回的 promise 将会 rejected ,但是没有捕捉到这个 rejection 。意味着你的 app 的这个错误被吞了。
出于这个原因,有些人认为上面的代码是反模式的,并推荐下面的代码:
1 | save() |
差异很微妙但是很重要。在第一个示例中, 源自 save() 的错误将会被捕获, 但源自 handleSuccess() 的错误将会被吞掉。

在第二个示例中,.catch() 可以处理 save() 和 handleSuccess() 两者的 rejection 。

当然, save() 的错误可能是网络错误,但 handleSuccess() 的错误可能是开发人员忘记处理特定的状态码。如果你想以不同的方式处理它们,你可以选择这样处理:
1 | save() |
无论你喜欢哪一种方式,我都建议在所有的 promise 链都是以 .catch() 结尾。重复这么做是值得的:
I recommend ending all promise chains with a .catch().
如何取消一个 Promise?
promises 萌新经常想知道的第一件事是如何取消一个 promise。这是一个想法:以 “Cancelled” 为原因 reject promise 。如果你想和 “normal” 错误以不同的处理方式,在 error handler 中添加一个处理分支。
以下是人们在取消 promise 时所犯的一些常见错误:
在 promise 中添加 .cancel()
添加 .cancel() 使 promise 不符合标准,而且也违反了一些 promises 的规则:只有创建 promise 的函数才能 resolve, reject,或者 cancel 这个 promise 。暴露出来会破坏 promise 的封装,并且会鼓励人们在不应该知道 promise 的地方编写操作 promise 的代码。避免意大利面式的代码以及破坏 promises 。
忘记清理
有一些聪明的人发现可以使用 Promise.race() 组委取消机制。问题是使用了创建 promise 的函数来做取消控制,只有这个函数可以清理活动内容,例如清理 timeouts 或者清理数据引用的内存空间等等。
忘记处理 rejected 状态下取消 promise
你知道吗当你忘记处理 promise rejection , Chrome 控制台会抛出一个警告信息。
过度复杂
withdrawn TC39 proposal 提出了使用独立的消息通道实现取消 promise 。它提出了一个叫做 cancellation token 的新概念。在我看来,这个解决方案会让 promise 规范变得臃肿,它的唯一作用是分离了 rejections 和 cancellations ,这是没有必要的。
你是否希望基于 exception 或者 cancellation 来进行切换?这确实是 promise 的工作?我认为不是。
重新思考 Promise Cancellation
通常,在 promise 创建时传入所有需要的信息以确定如何 resolve / reject / cancel 这个promise 。这样就没有必要在 promise 上添加 .cancel() 函数了。你可能会有疑问,在 promise 创建的时候怎么知道是否可以取消。
“If I don’t yet know whether or not to cancel, how will I know what to pass in when I create the promise?”
如果只有某种对象可以代替未来的潜在的值…等一下。
我们传入的那个代表是否取消的值可以是一个 promise 。可能是这样:
我们使用的默认参数默认是不取消的。cancel 参数是可选的。像之前一样设定一个 settimeout ,不过会保存这个 timeout 的 ID 以便于在之后清理。
我们使用 cancel.then() 方法来处理 cancellation 和资源清理。这个方法只会在 promise resolve 之前取消了才会执行。如果你太晚取消,你就错过了你的机会。火车已经驶离了车站。
注意:你可能想知道
noop()函数是做什么的。单词 noop 代表了 no-op,意思是空函数什么都不做。如果没有这个函数,V8 引擎会抛出警告:UnhandledPromiseRejectionWarning: Unhandled promise rejection。永远都要处理 promise rejections 是一个好主意,即使是noop()函数。
Note: You may be wondering what the noop() function is for. The word noop stands for no-op, meaning a function that does nothing. Without it, V8 will throw warnings: UnhandledPromiseRejectionWarning: Unhandled promise rejection. It’s a good idea to always handle promise rejections, even if your handler is a noop().
抽象 Promise Cancellation
这对于 wait() 计时器是很好的,但是我们可以进一步抽象这个想法,封装所有你需要的:
默认拒绝 cancel promise – 如果没有传入 cancel 到 promise ,我们不希望 cancel 或抛错。
记住因 cancellations 而 reject 时要进行清理。
记住
onCancel的清理工作本身也会抛错,这个错误也需要处理。(注意上面的示例中省略了错误处理 – 很容易忘记!)
让我们来创建一个可取消的 promise 工具,你可以使用它来包装任何 promise 。例如,处理网络请求等… 方法前面如下所示:
1 | speculation(fn: SpecFunction, shouldCancel: Promise) => Promise |
SpecFunction 就像你传入 Promis 的构造函数一样,有一点不同 – 它有一个 onCancel() handler:
1 | SpecFunction(resolve: Function, reject: Function, onCancel: Function) => void |
注意这个例子只是向你解释它是如何工作的。还有一些其他边界问题需要考虑。例如,这个版本的代码,promise 已经固定了,取消 promise 依然会调用 handleCancel 函数。
我实现了一个生产版本,处理了边界问题的开源库,Speculation .
让我们使用这个改进的抽象库来重写之前的 wait() 工具函数。首先安装 speculation:
1 | npm install --save speculation |
现在你可以导入和使用它:
这简化了一些,因为你不需要关心 noop() , 捕获 onCancel() ,函数或其他边界问题的错误。这些细节都被抽象到 speculation() 中了。随时在真实项目中使用它。
原生 JS Promise 的附加内容
原生 Promise 对象有一些你可能感兴趣的附加内容:
Promise.reject()返回一个 rejected promise 。Promise.resolve()返回一个 resolved promise 。Promise.race()接收一个 promise 数组 (或任何迭代器) 并且返回第一个 resolved (值) 或 rejected (原因) 的 promise 。Promise.all()接收一个 promise 数组 (或任何迭代器) 并且当迭代器中所有 promises 都 resolved 后返回一个 promise ,或者返回第一个 rejected (原因) 的 promise。
总结
Promises 已称为 JavaScript 语法的重要部分,包括用于 ajax 请求的 WHATWG Fetch 标准,用于让异步代码看起来像同步代码的 Async Functions 标准。
在撰写本文时 (2017.1.23),Async functions 还处于第3阶段,但我预测它们很快将成为 JavaScript 中异步编程的一种非常流行,非常有效的解决方案 – 这意味着在不久的将来,学习 promises 对 JavaScript 开发人员来说将变得更加重要。
例如,如果你正在使用 Redux ,我建议你看下 redux-saga:一个用于管理 Redux 中副作用的库,整个文档都依赖于异步函数。
我希望即使是有着丰富 promise 使用经验的开发人员在阅读完本文之后,能够对 promise 有更好的理解和 promise 是如何工作的,以及更好地去使用 promise 。