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一旦决议一直保持其决议结果不变,可多次查看。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容