我们确定了通过回调表达程序异步和管理并发的两个主要缺陷:缺乏顺序性和可信任性。
首先解决控制反转问题。如果我们能把控制反转在反转回来呢?不把自己的continuation传给第三方,而是希望第三方给我们提供了解其何时结束的能力,然后我们自己的代码来决定下一步做什么。
这种范式就称为Promise
现在值与将来值
var x, y = 2;
console.log( x + y); // NaN <-- 因为x还没有定义
来看下Promise函数表达这个x + y的例子:
function add(xPromise, yPromise){
// Promise.all([..])接受一个promise数组并返回一个新的promise
// 这个新promise等待数组中的所有promise完成
return Promise.all( [xPromise, yPromise] )
//这个promise决议之后,我们取得收到的x和y值并加在一起
.then( function(values){
// values是来自于之前决议的promise的消息数组
return values[0] + values[1];
})
}
// fetchX() 和 fetchY()返回相应值的promise,可能已经就绪
// 也可能以后就绪
add(fetchX(), fetchY())
// 我们得到一个这两个数组的和的promise
// 现在链式调用 then() 来等待返回promise的决议
.then( function(sum){
console.log( sum ); // 这更简单
})
这躲代码中有两层Promise
fetchX() 和 fetchY() 是直接调用的,它们返回值(promise)被传给add()。
第二层是add(..)(通过Promise.all([..]))创建并返回的promise。我们通过调用then(..)等待这个promise。
在add(..)内部,Promise.all([..])调用创建了一个promise(这个promise等待promiseX 和 promiseY的决议)。链式调用.then(..)创建了另外一个promise。这个promise由return values[0] + values[1]这一行立即决议(得到加运算的结果)。因此,链add(..)调用终止处的调用then(..)--在代码结尾处--实际上操作的是返回的第二个promise,而不是由Promise.all([..])创建的第一个promise。还有,尽管第二个then(..)后面没有链接任何东西,但它实际上也创建了一个新的promise。
通过Promise,调用then(..)实际上可接受两个函数,第一个用于完成情况,第二个用于拒绝情况
add( fetchX(), fetchY() )
.then(
// 完成处理函数
function(sum){
console.log( sum );
},
//拒绝处理函数
function(err){
console.error( err );
}
)
从外部看,由于Promise封装了依赖于时间的状态 - 等待底层值的完成或拒绝,所以Promise本身是与时间无关的。因此,Promise可按照预测的方式组成(组合),而不用关心时序或底层的结果。
另,一旦Promise决议,它就永远保持在这个状态。此时它就成为了不变值(immutable value),可根据需求多次查看。
Promise 是一种封装和组合未来值的易于复用的机制。
3.1.2 完成事件
假定要调用一个函数foo(..)。我们不知道也不关心它的任何细节。这个函数可能立即完成任务,也可能需要一段时间才能完成。
我们只需要知道foo(..)什么时候结束,这样就可进行下一个任务。
在典型的JS风格中,如果需要侦听某个通知,你可能会想到事件。因此,可把对通知的需求程序组织为对foo(..)发出的一个完成事件(completion event, 或 continuation 事件)的侦听。
使用回调的话,通知就是任务(foo(..))调用的回调。而使用Promise的话,我们把这个关系反转了过来,侦听来自foo(..)的事件,然后在得到通知的时候,根据情况继续。
考虑下伪代码
foo(x){
// 开始做点可能耗时的工作
}
foo(42)
on( foo "completion" ){
// 可进行下一步了
}
on( foo "error" ){
// 啊,foo(..)出错了
}
从本质上讲,foo(..)并不需要了解调用代码订阅了这些事件,这样就很好地实现了关注点分离
。
遗憾的是,JS并不存在这种环境。
以下是JS中更自然的表达方法:
function foo(x){
// 开始做点什么耗时的工作
// 构造一个listener事件通知处理对象来返回
return listener;
}
var evt = foo(42);
evt.on("completion", function(){
// 可进行下一步了
});
evt.on('failure', function(err){
// 啊,foo(..)中出错了
});
foo(..)显式创建返回了一个事件订阅对象,调用代码得到这个对象,并在其上注册了两个事件处理函数。
相对面向回调的代码,这里的反转是显而易见的,而且这也是有意为之。这里没把回调传给foo(..),而是返回一个名为evt的事件注册对象,由它来接受回调。
所以对回调模式的反转实际上是对反转的反转,或者称为反控制反转
-把控制返还给调用代码,这也是我们最开始想要的效果。一个很重要的好处是,可把这个事件侦听对象提供给代码中多个独立的部分; 在foo(..)完成的时候,它们都可独立地得到通知,以执行下一步:
var evt = foo(42);
// 让 bar() 侦听 foo() 的完成
bar( evt );
// 并且让baz() 侦听foo() 的完成
baz( evt );
对控制的反转的恢复实现了更好的关注点分离,其中bar()和baz()不需要牵扯到foo()的调用细节。类似地,foo()不需要知道或关注bar()和baz()是否存在,或者是否在等待foo()的完成通知。
从本质上说,evt对象就是分离的关注点之间一个中立的第三方协商机制。事件侦听对象evt就是Promise的一个模拟。
function foo(x){
// 做一些可能耗时的工作
// 构造并返回一个Promise
return new Promise( function(resolve, reject){
// 最终调用resolve() 或者 reject()
// 这是这个Promise的决议回调
})
}
var p = foo(42)
bar( p );
baz( p );
new Promise( function(..){..} )模式通常称为revealing constructor 。传入的函数会立即执行,它有两个参数。这些是promise的决议函数。resolve(..)通常标识完成,而reject()则标识拒绝。
你可能会猜测bar(..) 和 baz(..) 的内部实现或许如下:
function bar(fooPromise){
// 侦听foo(..)完成
fooPromise.then(
function(){
// foo() 已经完毕,所以执行bar() 任务
},
function(){
// 啊,foo(..) 中出错了!
}
)
}
另一种实现方式是:
function bar(){
// foo(..) 肯定已经完成,所以执行bar()的任务
}
function oopsBar(){
// 啊,foo()中出错了,所以bar()没有运行
}
// 对于baz()和oopsBaz()也是一样
var p = foo(42);
p.then(bar, oopsBar);
p.then(baz, oopsBaz);
最主要的区别在于错误处理部分。
在第一段里,不论foo()成功与否,bar()都会被调用。并且如果foo()失败的话,它会亲自处理自己的回退逻辑。
第二段里,bar()只有在foo()成功时才会被调用,否则就会调用oopsBar()。
这两种方法本身并谈不上对错,只是各自适用不同的情况。
不管哪种情况,都是从foo()返回的promise p 来控制接下来的步骤。
另外,两段代码都以使用promise p 调用then()两次结束。这个事实说明可前面的观点,就是promise一旦决议一直保持其决议结果不变,可多次查看。
网友评论