3.3 Promise 信任问题
回顾一下只用回调编码的信任问题,把一个回调传入工具foo()时可能出现如下问题:
- 调用回调过早
- 调用回调过晚(或不被调用)
- 调用回调次数过少或过多
- 未能传递所需的环境和参数
- 吞掉可能出现的错误和异常
Promise 的特性就是专门用来为这些问题提供一个有效的可复用的答案。
3.3.1 调用过早
根据定义,Promise就不必担心这种问题,因为即使是立即完成的Promise(类似于 new Promise(function(resolve){ resolve(42); }) )也无法被同步观察到。
也就是说,对一个Promise调用then()的时候,即使这个Promise已经决议,提供给then()的回调也总会被异步调用。
3.3.2 调用过晚
Promise创建对象调用resolve()或reject()时,这个Promise的then()注册的观察回调就会被自动调度。可确信,这些被调度的回调在下一个异步事件点上一定会被触发。
同步查看是不可能的,所以一个同步任务链无法以这种方式运行来实现按照预期有效延迟另一个回调的发生。也就是说,一个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”,这是因为Promise的运作方式。
Promise 调度技巧
有很多调度的细微差别。这种情况下,两个独立Promise上链接的回调的相对顺序无法可靠预测。
如果两个Promise p1 和 p2都已经决议,那么p1.then(), p2.then()应该最终会制调用p1的回调,然后是p2。但还有一些微妙的场景可能不是这样。
var p3 = new Promise( function(resolve, reject){
resolve( "B" );
});
var p1 = new Promise(function(resolve, reject){
resolve( p3 );
})
p2 = new Promise(function(resolve, reject){
resolve( "A" );
})
p1.then( function(v){
console.log( v );
})
p2.then( function(v){
console.log( v );
})
// A B , 而不是像你认为的 B A
p1不是用立即值而是用另一个promise p3决议,后者本身决议为值“B”。规定的行为是把p3展开到p1,但是是异步地展开。所以,在异步任务队列中,p1的回调排在p2的回调之后。
要避免这样的细微区别带来的噩梦,你永远都不应该依赖于不同Promise间回调的顺序和调度。实际上,好的编码实践方案根本不会让多个回调的顺序有丝毫影响,可能的话就要避免。
3.3.3 回调未调用
首先,没有任何东西(甚至JS错误)能阻止Prmise向你通知它的决议(如果它决议了的话)。如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总是会调用其中一个。
当然,如果你的回调函数本身包含JS错误,那可能就会看不到你期望的结果。但实际上回调还是被调用了。后面讨论,这些错误并不会被吞掉。
但是,如果Promise永远不决议呢?即使这样,Promise也提供了解决方案。其使用了一种称为竟态的高级抽象机制:
// 用于超时一个Promise的工具
function timeoutPromise(delay){
return new Promise( function(resolve, reject){
setTimeout(function(){
reject("Timeout!");
}, delay)
})
}
// 设置foo()超时
Promise.race( [
foo(),
timeoutPromise( 3000 );
])
.then(
function(){
// foo() 及时完成!
},
function(err){
// 或者foo()被拒绝,或者只是没能按时完成
// 查看err来了解是哪种情况
}
)
我们可保证一个foo()有一个信号,防止其永久挂住程序。
3.3.4 调用次数过少或过多
根据定义,回调被调用的正确次数应该是1。“过少”的情况就是调用0次,和前面解释过的“未被”调用是同一种情况。
“过多”容易解释。Promise的定义方式使得它只能被决议一次。如果出于某种原因,Promise创建代码试图调用resolve()或reject()多次,或者试图两者都调用,那么这个Promise将只会接受第一次决议,并默默地忽略任何后续调用。
由于Promise只能被决议一次,所以任何通过then()注册的(每个)回调就只会被调用一次。
当然,如果你把同一个回调注册了不止一次(比如p.then(f); p.then(f)),那头被调用的次数就会和注册次数相同。响应函数只会被调用一次。
3.3.5 未能传递参数/环境值
Promise 至多只能有一个决议值(完成或拒绝)。
如果你没有用任何值显式决议,那么这个值就是undefined,这是JS常见的处理方式。但不管这个值是什么,无论当前或未来,它都会被传给所有注册的(且适当的完成或拒绝)回调。
还有一点需要清楚:如果使用多个参数调用resovel()或者reject()第一个参数之后的所有参数都会被默默忽略。
如果要传递多个值,你就必须要把它们封装在一个数组或对象中。
对环境来说,JS中的函数总是保持其定义所在的作用域的闭包,所以它们当然可继续你提供的环境状态。
3.3.6 吞掉错误或异常
如果在Promise的创建过程中或在查看其决议结果过程中的任何时间点上出现了一个JS异常错误,比如一个TypeError或RefernceError,那这个异常就会被捕捉,并且会使这个Promise被拒绝。
var p = new Promise( function(resolve, reject){
foo.bar(); // foo 未定义,所以会出错
resolve(42); // 永远不会到达这里
});
p.then(
function fulfilled(){
// 永远不会到这里
},
function rejected(err){
// err 将会是一个TypeError异常对象来自foo.bar()这一行
}
)
foo.bar()中发生的JS异常导致了Promise拒绝,你可捕捉并对其做出响应。
Promise甚至把JS异常也变成了异步行为,进而极大降低了竟态条件出现的可能。
但是,如果Promise完成后在查看结果时(then()注册回调中)出现了JS异常错误会怎样呢?
var p = new Promise( function(resolve, reject){
resolve( 42 );
});
p.then(
function fulfilled(msg){
foo.bar();
console.log( msg ); // 永远不会到达这里
},
function rejected(err){
// 永远也不会到达这里
}
)
等一下,这看qvnn来像是foo.bar()产生的异常真的被吞掉了。别担心,实际上并不是这样。但是这里有一个深的问题。就是我们没有侦听到它。p.then()调用本身返回了另一个promise,正是这个promise将会因TypeError异常而被拒绝。
3.3.7 是可信任的 Promise 吗
你肯定已经注意到Promise并没有完全摆脱回调。它们只是改变了传递回调的位置。我们并不是把回调传递给foo(),而是从foo()得到某个东西,然后把回调传给这个东西。
但是,为什么这就比单纯使用回调更值得信任呢?
关于Promise的很重要但是常常被忽略的一个细节是,Promise对这个问题已经有一个解决方案。包含在原生ES6 Promise实现中的解决方案就是Promise.resolve()。
如果向Promise.resolve()传递一个非Promise、非thenable的立即值,就会得到一个用这个值填充的promise。下面这种情况下,promise p1 和 promise p2 的行为是完全一样的:
var p1 = new Promise( function(resolve, reject){
resolve( 42 );
} )
var p2 = Promise.resolve(42);
而如果向Promise.resolve() 传递一个真正的Promise,就只会返回同一个promise
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( p1 );
p1 === p2; // true
如果向Promise.resolve()传递了一个非Promise的thenable 值,前者会试图展开这个值,而且展开过程会持续到提取出一个具体的非类Promise的最终值。
var p = {
then: function(cb){
cb( 42 );
}
};
// 这可以工作,但只是因为幸运而已
p
.then(
function fulfilled(val){
console.log( val ); //42
},
function rejected(err){
// 永远不会到这里
}
)
但是,下面又会怎样呢?
var p = {
then: function(cb, errcb){
cb(42);
errcb("evil laugh");
}
};
p
.then(
function fulfilled(val){
console.log( val ); //42
},
function rejected(err){
// 啊,不应该运行!
console.log( err ); // 邪恶的笑
}
)
尽管如此,我们还是都可把这些版本的p 传给Promise.resolve(),然后就会得到期望中的规范化后的安全结果:
Promise.resolve(p)
.then(
function fulfilled(val){
console.log(val); //42
},
function rejected(err){
// 永远不会到这里
}
)
Promise.resolve()可接受任何thenable,将其解封完它的非thenable值。从Promise.resolve()得到的是一个真正的Promise,是一个可信任的值。如果你传入的已经是真正的Promise,那们你得到的就是它本身,所以通过Promise.resolve()过滤来获得可信任性完全没有坏处。
假设我们要调用一个工具foo(),且不确定得到的返回值是否是一个可信任的行为良好的Promise,但我们可知道它至少是一个thenable。Promise.resolve()提供了可信任的Promise封装工具,可链接使用:
// 不要这么做
foo(42)
.then(function(v) {
console.log( v );
});
// 而要这么做
Promise.resolve( foo(42) )
.then( function(v){
console.log(v)
})
对于用Promise.resolve() 为所有函数的返回值都封装一层。另一个好处是,这样做很容易把函数调用规范为定义良好的异步任务。如果foo(42)有时会返回一个立即值,有时会返回Promise,那么Promise.resolve(foo(42))就能保证总返回一个Promise结果。