- 前面,我们确定了通过回调表达程序异步和管理并发的两个主要缺陷:缺乏顺序性和可信任性。
- 首先要解决的是控制反转的问题,如果能够把控制反转再反转回来,那将会怎么样?这种范式称为Promise。
- 目前绝大多数 JavaScript/DOM 平台新增的异步API都是基于
Promise
构建的,所以深入学习Promise很有意义。
一、什么是Promise
1. 未来值
现在值与将来值
- 语句有可能是现在完成,也可能是将来完成。
- 为了统一处理现在和将来,我们把它们都变成将来,即所有操作都是异步。
2. Promise值
Promoise
是一种封装和组合未来值的易于复用的机制。
决议
-
Promise的决议结果可能是拒绝而不是完成。拒绝值和完成的Promise不一样:完成值总是编程给出的,而拒绝值,通常称为拒绝原因。
-
通过Promise,调用
then()
实际上可以接受两个函数,第一个用于完成情况,第二个用于拒绝情况。 -
由于Promise本身与时间无关,依赖于状态——等待底层完成或拒绝。因此,Promise可以按照可预测的方式组成,而不用关心时序或底层的结果。
不变值
-
一旦Promise决议,它就永远保持这个状态。此时它就成为了不变值,可以根据需要多次查看。
-
Promise决议后就是外部不可变的值,我们可以安全地把这个值传递给第三方,并确信它不会被有意无意地修改。
3. 完成事件
-
也可以从另一个角度看待Promise的决议:一种在异步任务中作为两个或更多步骤的流程控制机制,时序上的this-then-that。
-
我们只需要知道
foo()
函数什么时候结束,这样就可以进行下一个任务。(假设foo()
为耗时操作) -
使用回调的话,通知的是
foo()
调用回调。而使用Promise的话,我们把这个关系反转了过来,侦听来自foo()
的完成事件,然后在得到通知的时候,根据情况继续。
二、具有then方法的鸭子类型
- 根据一个值的型态(具有哪些属性)对这个值的类型做出一些假定,这种类型检查一般用术语鸭子类型来表示。
- 在Promise领域,一种重要的细节是如何确定某个值是不是真正的Promise。或者说,是不是行为类似于Promise。
p instanceof Promise
不足以作为检查方法。- 识别Promise就是定义某种成为
thenable
的东西,将其定义为任何具有then()方法的对象和函数。
三、Promise的信任问题
回顾一下回调编码的信任问题:
- 调用回调过早
- 调用回调过晚(或没有调用)
- 调用次数过多或过少
- 没有成功地将参数传入到回调中
- 吞掉可能出现的报错和异常
Promise的特性就是专门用来为这些问题提供一个有效的可复用的答案。
1. 调用过早
- 原因:一个任务有时同步完成,有时异步完成。
- Promise就不必担心这种问题,即使是立即完成的Promise,then方法中的回调还是异步调用的。
2. 调用过晚
- 一个Promise决议后,这个Promise上所有的通过then()注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。
p.then(function() {
p.then(function() {
console.log("C")
})
console.log("A")
})
p.then(function() {
console.log("B")
})
// A B C
这里C无法打断或抢占B。
3. 回调未调用
-
没有任何东西(甚至JavaScript)能阻止Promise向你通知它的决议。如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总会调用其中一个。
-
但是回调本身包含错误,就看不到期望的结果,当回调还是会调用。
-
如果Promise本身永远不会被决议呢?
-
使用一种称为竞态的高级抽象机制。
Promise.race([
foo(), // 可能不会有结果
timeoutPromise(3000); // 3秒后调用reject
])
4. 调用次数过多或过少
- 由于Promise只能被决议依次,所以任何通过then()注册的回调都只会调用一次。
5. 是可信任的Promise吗
- Promise并没有摆脱回调,只是改变了传递回调的位置,并不是把回调传递给
foo()
,而是从foo()
上得到某个东西(Promise),然后把回调传递给它。
(原文还有其他问题讨论)
四、链式流
- 每次对Promise调用
then()
,它都会创建并返回新的Promise,我们可以将其链接起来。 - 我们构建的Promise链不仅是一个表达多步异步序列的流程控制,还是一个从步骤到下一个步骤传递消息的消息通道。
如果某个步骤出错怎么办?
- 调用
then()
时的完成处理函数或拒绝处理函数如果抛出异常,都会导致下一个Promise因为这个异常而立即被拒绝。 - 如果你调用Promise的
then()
只传入一个完成处理函数,一个默认拒绝处理函数就会顶替上来。 - 因此,错误可以沿着Promise链传播下去,直到遇到显式定义的拒绝出来函数。
- 同样,也有默认完成处理函数。
五、错误处理
-
同步的
try...catch
结构无法用于异步代码模式。 -
Promise采用分离回调风格,一个回调用于完成情况,一个回调用于拒绝情况。
1. 绝望的陷阱
- Promise错误处理是一个“绝望陷阱”,Promise状态会吞掉所有错误,如果你忘记查看状态,这个错误就会默默地消失。
- 所以,一些开发者建议在Promise链末尾使用
Promise.catch()
来捕获错误,但是catch中的错误就无法捕获了。
2. 处理未捕获的情况
- 这不是一个容易解决的问题。
- 有些Promise库增加了一些方法,用于类似于“全局未处理拒绝”处理函数的东西,这样就不会抛出全局错误,而是调用这个函数。
- 在多数情况下使用良好,因为在Promise拒绝和检查拒绝之间没有很长的延迟。
六、Promise API概述
1. new Promise()构造器
构造器 Promise()
必须和new
一起使用
var p = new Promise(function(resolve, reject) {
// 这部分立即执行
// ...
// resolve() 用于完成这个Promise
// reject()用于拒绝这个Promise
})
2. Promise.resolve()和Promise.reject()
-
Promise.resolve()
用于创建一个已完成的Promise。如果传入的值是Promise,则什么都不做,直接把这个值返回。 -
Promise.reject()
用于创建一个拒绝的Promise。与上面在构造器内调用reject()
等价。
3. then()和catch()
-
then()
接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。如果两者中的任何一个被省略或者作为非函数值传入的话,就会替换为相应的默认回调。 -
默认完成回调只是把消息传递下去,而默认拒绝则只是重新抛出其接收到的出错原因。
-
catch()
只接受拒绝回调作为参数,相当于then(null, ...)
-
then()
和catch()
也会创建并返回一个新的Promise。这个Promise用于实现Promise链式流程控制。
4. Promise.all()
- 在所有成员都完成才会完成,如果有一个拒绝,就会立刻拒绝。
- 这个数组可以是Promise,也可以是立即值,会通过
Promise.solve()
过滤,立即值会被规范为Promise。
5. Promise.race()
- 只需要第一个完成的,其他的抛弃。
- 那些被忽略的Promise不会取消,结果会被默默忽略。
超时竞赛:设置Promise超时(前面有例子说明)
七、Promise局限性
1. 顺序错误处理
- 如果一个Promise链中没有错误处理,那么错误很容易被无意中默默忽略掉。
- 还有注意变量的指向
var p = foo(42) // 这里的foo()代表构造Promie
.then(STEP2)
.then(STEP3)
这里的P指向的不是第一个Promise,而是STEP3
p.catch() // 可以捕获到上面任意步骤的错误
2. 单一值
- Promise只能有一个完成值或一个拒绝理由。在简单例子中没有问题,但是在更复杂的场景中,你可能会发现这是一种局限。
3. 单决议
- Promise只能决议一次,完成或拒绝。在许多异步情况中,你只会获取一个值一次,所以工作良好。
- Promise无法支持多值决议处理。
4. 无法取消Promies
- 一旦创建了一个Promise并为其注册了完成和拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,你也没有办法从外部停止它的进程。
- 单独的Promise不应该可取消,但是取消一个序列是合理的。因为Promise值是一个不变的值(状态),而Promise序列不会有这种考虑。