03 Promise

我们确定了通过回调表达程序异步和管理并发的两个主要缺陷:缺乏顺序性和可信任性。
首先解决控制反转问题。如果我们能把控制反转在反转回来呢?不把自己的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一旦决议一直保持其决议结果不变,可多次查看。

你可能感兴趣的:(03 Promise)