[译] 深刻理解 Promise 五部曲:5. LEGO

原文地址:http://blog.getify.com/promis...javascript

Part4:扩展问题 中,我讨论了如何扩展和抽象Promise是多么的常见,以及这中间的一些问题。可是为何promise对于开发者来讲不是足够友好的呢?这就是它的设计用意吗?java

I've Got Friends In Low Places

Promise被设计为低级别的构建块。一个promise就像一个乐高玩具。单个乐高只是一个有趣的玩具。可是若是把它们拼在一块儿,你会感觉到更多的乐趣。react

问题是promise不是你小时候玩儿的那个有趣LEGO,它们不是充满想象力的打气筒,也不是Gandalf mini-figure(一种乐高玩具)。git

都不是,promise只是你的简单老旧的4X2的砖块。github

这并非使它们很是有用。可是它们是你箱子中最重要的组成部分之一。当它们彼此分开时它们只是这么个东西,可是当把它们整合在一块儿它们就会散发出光芒。ajax

换句话说,promise本质上是一个构建在真实用户之上的低级别的API。这是对的:promise并非给开发者使用的,它们是给库做者使用的。segmentfault

你会从它们那收益许多,可是你极可能不是直接使用它们。你将会使用的是通过许多库组合包装以后的结果。数组

控制 VS 值

请容许我矫正第一个最大的关于promise的误解:它们不是真正关于流程控制的promise

promise固然能够连接在一块儿来变成近似异步流程控制的东西。可是最后证实它们并不像你想象的那样擅长这个任务。promises确实只是一个值的容器。这个值可能如今就存在也多是将来的一个值。可是无论怎样,它只是一个值。这是promise最有意义的好处之一。它们在值的上面建立了一个强大的抽象使得值再也不是暂存的东西。换句话说,无论那个值如今是否存在,你均可以用一样的方式使用promise。在这个系列的 第三部分 中,我讨论过promise必须是不可变的,它们做为值的意义也是基于这个特色的。app

promises就像状态的小型的自包含的表现方式。它们是可组合的,也就意味着你所有的程序能够用它们来表示。

限制

就像你不能奢望一个单独的4X2的乐高能够变成一个跑车,让promise成为你的异步流程控制机制也是一种奢望。
那么promises做为一个非暂存的不可变的值对于解决异步任务意味着什么呢?在它们设计哲学的约束中,有它们擅长而且能够有帮助的东西。

在剩下的内容中,我会讨论这个限制。可是我并不打算做为一个promise的批判者。我试图去强调扩展和抽象的重要性。

错误处理

当我说promise只是一个值的容器的时候我撒了个小慌。实际上,它是一个成功值或者失败信息的容器。在任什么时候候,一个promise是一个将来的成功值或者在获取这个值时的失败信息。不会超过这两种状况。

在某种意义上说,一个promise是一个决策结构,一个if..then..else。其余人喜欢把它想成一个try..catch结构。无论是哪一种理解,你就像在说"请求一个值,无论成功仍是失败"。

就像尤达说,"Do or do not, there is no try"。

考虑下面这个状况:

function ajax(url) {
    return new Promise( function(resolve,reject){
        // make some ajax request
        // if you get a response, `resolve( answer )`
        // if it fails, `reject( excuses )`
    } );
}

ajax( "http://TheMeaningOfLife.com" )
.then(
    winAtLife,
    keepSearching
);

看到winAtLife()keepSearching()函数了吗?咱们在说,"去问问生命的意义,无论你有没有找到答案,咱们都继续"。

若是咱们不传入keepSearching会怎样?除了做为一个乐观主义者假设你会找到答案而后在生命长河中取胜,这里会有什么危险呢?

若是promise没有找到生命的意义(或者若是在处理答案的过程当中发生了javascript异常),它会默默地保留着错误的事实,也许会永远保留着。就算你等上一百万年,你都不会知道对于答案的请求失败了。

你只能经过观察才能知道它失败了。这可能须要深刻到形而上学或者量子学的东西。让咱们中止在这吧。

因此不带失败处理函数的promise是一个会默默地失败的promise。这并很差。这意味着若是你忘记了,你会陷入失败的陷阱而不是成功。

因此你会怀疑:为何promises会忽略失败处理函数呢?由于你可能如今不在乎失败的状况,只有之后某个时刻会关心。咱们程序的暂时性意味着系统如今不会知道你之后会想作什么。如今忽略失败处理函数也许对你来讲是正合适的,由于你知道你会把这个promise连接到另外一个promise,而且那个promise有一个失败处理函数。

因此promise机制让你能够建立不须要监听失败的promise。

这里有一个很微妙的问题,极可能也是大多数刚接触promise的开发者会碰到的问题。

束缚咱们的链子

为了理解这个问题,咱们首先须要理解promises是如何连接在一块儿的。我认为你会很快明白promise链是强大而且有一点复杂的。

ajax( "http://TheMeaningOfLife.com" )
.then(
    winAtLife,
    keepSearching
)
// a second promise returned here that we ignored!
;

ajax(..)调用产生了第一个promise,而后then(..)调用产生了第二个promise。咱们没有捕捉而且观察在这段代码中的第二个promise,可是咱们能够。第二个promise是根据第一个promise处理函数如何运行来自动变成fulfilled状态(成功或者失败)。

第二个promise不会在乎第一个promise是成功仍是失败。它在乎第一个promise的处理函数(无论成功仍是失败)。
这是promise链的关键。可是这有一点很差理解,因此重复读上面那段话直到你理解为止。

考虑下promise代码一般是怎么写的(经过链):

ajax( ".." )
.then( transformResult )
.then(
    displayAnswer,
    reportError
);

这段代码也能够像下面这么写,效果是同样的:

var promiseA = ajax( ".." );

var promiseB = promiseA.then( transformResult );

var promiseC = promiseB.then(
    displayAnswer,
    reportError
);

// we don't use `promiseC` here, but we could...

Promise A是惟一在乎ajax(..)结果的promise。

Promise B只关心Promise A在transformResult(..)函数内部是如何处理的(不是Promise A的结果自己),一样的,Promise C只关心Promise B在displayAnswer(..)或者reportError(..)函数内部是如何处理的(不是Promise B结果自己)。

再一次,重复读这段话直到理解。

transformResult(..)内部,若是它马上完成了它的任务,而后Promise B就会马上完成,无论成功仍是失败。然而,若是transformResult(..)不能马上完成,而是建立它本身的promise,咱们称它为Promise H1('H'是'hidden',由于它是隐藏在内部的)。本来Promise B返回的等待咱们如何处理Promise A的promise,如今概念上被Promise H1替换了(并非真的替换了,只是被说成同样的)。

因此,如今当你说promiseB.then(..)时,它实际上就像说promiseH1.then(..)。若是Promise H1成功了,displayAnswer(..)会被调用,可是若是它失败了,reportError(..)会被调用。

这就是promise链是如何工做的。

可是,若是Promise A(由ajax调用返回)失败了会怎样?promiseA.then(..)调用没有注册一个失败处理函数。它会默默地隐藏错误吗?它会的,除了咱们连接上Promise B而后在上面注册一个错误处理函数:reportError(..)。若是Promise A失败了,transformResult(..)不会被调用,而且没有错误处理函数,因此Promise B立刻被标记为失败,因此reportError(..)会被调用。

若是Promise A成功了,transformResult(..)会被执行,而后当运行transformResult(..)时有一个错误会怎样?Promise B被标记为失败,而后reportError(..)也会被调用。

可是这里是危险的地方,这个地方甚至有经验的开发者都会遗漏的!

若是Promise A成功了(成功的ajax(..)),而后Promise B成功了(成功的transformResult(..)),可是当运行displayAnswer(..)时有一个错误会怎样?

你也许会认为reportError(..)会被调用?大多数人会这么想,可是不是的。

为何?由于来自displayAnswer(..)的一个错误或者失败promise致使一个失败的Promise C。咱们监听Promise C失败的状况了吗?仔细看看。没有。

为了确保你不会漏掉这种错误而且让它默默地隐藏在Promise C状态内部,你也会但愿监听Promise C的失败:

var promiseC = promiseB.then(
    displayAnswer,
    reportError
);

// need to do this:
promiseC.then( null, reportError );

// or this:, which is the same thing:
promiseC.catch( reportError );

// Note: a silently ignored *Promise D* was created here!

OK,因此如今咱们捕获displayAnswer(..)内部的错误。不得不去记住这个有一点坑爹。

乌龟

可是有一个更加微妙的问题!若是当处理displayAnswer(..)返回的错误时,reportError(..)函数也有一个JS异常会怎样?会有人捕获这个错误吗?没有。

看!上面有一个隐含的Promise D,而且它会被告知reportError(..)内部的异常。

OMG,你确定会想。何时才能中止?它会这样一直下去吗?

一些promise库做者认为有必要解决这个问题经过让"安静的错误"被做为全局异常抛出。可是这种机制该如何得知你不想再连接promise而且提供一个错误处理函数呢?它如何知道何时应该通报一个全局异常或者不通报呢?你确定不但愿当你已经捕获而且处理错误的状况下仍然有不少控制台错误信息。

在某种意义上,你须要能够标记一个promise为“final”,就像说“这是我链子中的最后一个promise”或者“我不打算再连接了,因此这是乌龟中止的地方”。若是在链的最后发生了错误而且没有被捕获,而后它须要被报告为一个全局异常。

从表面上我猜想这彷佛是很明智的。这种状况下的实现像下面这样:

var promiseC = promiseB.then(
    displayAnswer,
    reportError
);

promiseC
.catch( reportError )
.done(); // marking the end of the chain

你仍然须要记住调用done(),要否则错误仍是会隐藏在最后一个promsie中。你必须使用稳固的错误处理函数。
"恶心",你确定会这么想。欢迎来到promises的欢乐世界。

Value vs Values

对于错误处理已经说了不少了。另外一个核心promsie的限制是一个promise表明一个单独的值。什么是一个单独的值呢?它是一个对象或者一个数组或者一个字符串或者一个数字。等等,我还能够在一个容器里放入多个值,就像一个数组或对象中的多个元素。Cool!

一个操做的最终结果不老是一个值,可是promise并不会这样,这很微妙而且又是另外一个失败陷阱:

function ajax(url) {
    return new Promise( function(resolve,reject){
        // make some ajax request
        // if you get a response, `resolve( answer, url )`
        // if it fails, `reject( excuses, url )`
    } );
}

ajax( ".." )
.then(
    function(answer,url){
        console.log( answer, url ); // ..  undefined
    },
    function(excuses,url){
        console.log( excuses, url ); // ..  undefined
    }
);

你看出这里面的问题了吗?若是你意外的尝试传递超过一个的值过去,无论传给失败处理函数仍是成功处理函数,只有第一个值能被传递过去,其余几个会被默默地丢掉。

为何?我相信这和组合的可预测性有关,或者一些其余花哨的词汇有关。最后,你不得不记住包裹本身的多个值要否则你就会不知不觉的丢失数据。

并行

真实世界中的app常常在“同一时间”发生超过一件事情。本质上说,咱们须要构建一个处理器,并行处理多个事件,等待它们所有完成再执行回调函数。

相比于promise问题,这是一个异步流程控制的问题。一个单独的promise不能表达两个或更多并行发生的异步事件。你须要一个抽象层来处理它。

在计算机科学术语中,这个概念叫作一个“门”。一个等待全部任务完成,而且不关心它们完成顺序的门。

在promise世界中,咱们添加一个API叫作Promise.all(..),它能够构建一个promise来等待全部传递进来的promise完成。

Promise.all([
    // these will all proceed "in parallel"
    makePromise1(),
    makePromise2(),
    makePromise3()
])
.then( .. );

一个相近的方法是race()。它的做用和all()同样,除了它只要有一个promise返回消息就执行回调函数,而不等待其余promise的结果。

当你思考这些方法的时候,你可能会想到许多方式来实现这些方法。Promise.all(..)Promise.race(..)是原生提供的,由于这两个方法是很经常使用到的,可是若是你还须要其余的功能那么你就须要一个库来帮助你了。限制的另外一个表现就是你很快就会发现你须要本身使用Array的相关方法来管理promise列表,好比.map(..).reduce(..)。若是你对map/reduce不熟悉,那么赶忙去熟悉一下,由于你会发现当处理现实世界中promise的时候你常常会须要它们。

幸运的是,已经有不少库来帮助你了,而且天天还有不少新的库被创造出来。

Single Shot Of Espresso,Please!

另外一个关于promise的事情是它们只会运行一次,而后就不用了。

若是你只须要处理单个事件,好比初始化一个也没或者资源加载,那么这样没什么问题。可是若是你有一个重复的事件(好比用户点击按钮),你每次都须要执行一系列异步操做会怎么样呢?Promise并不提供这样的功能,由于它们是不可变的,也就是不能被重置。要重复一样的promise,惟一的方法就是从新定义一个promise。

$("#my_button").click(function(evt){
    doTask1( evt.target )
    .then( doTask2 )
    .then( doTask3 )
    .catch( handleError );
});

太恶心了,不只仅是由于重复建立promise对于效率有影响,并且它对于职责分散不利。你不得不把多个事件监听函数放在同一个函数中。若是有一个方式来改变这种状况就行了,这样事件监听和事件处理函数就可以分开了。

Microsoft的RxJS库把这种方式叫作"观察者模式"。个人asynquence库有一个react(..)方法经过简单的方式提供了一个相似的功能。

盲区...

在一个已经被使用回调函数的API占据的世界中,把promise插入到代码中比咱们想象的要困难。考虑下面这段代码:

function myAjax(url) {
    return new Promise( function(resolve,reject){
        ajax( url, function(err,response){
            if (err) {
                reject( err );
            }
            else {
                resolve( response );
            }
        } )
    } );
}

我认为promise解决了回调地狱的问题,可是它们代码看起来仍然像垃圾。咱们须要抽象层来使得用promise表示回调变得更简单。原生的promise并无提供这个抽象层,因此结果就是经过原生promise写出来的代码仍是很丑陋。可是若是有抽象层那么事情就变得很简单了。

例如,个人asynquence库提供了一个errfcb()插件(error-first callback),用它能够构建一个回调来处理下面这种场景:

function myAjax(url) {
    var sq = ASQ();
    ajax( url, sq.errfcb() );
    return sq;
}

Stop The Presses!

有时,你想要取消一个promise而去作别的事情,可是若是如今你的promise正处在挂起状态会怎样呢?

var pr = ajax( ".." )
.then( transformResult )
.then(
    displayAnswer,
    reportError
);

// Later
pr.cancel(); //  <-- doesn't work!

因此,为了取消promise,你须要引入一下东西:

function transformResult(data) {
    if (!pr.ignored) {
        // do something!
    }
}

var pr = ajax( ".." )
.then( transformResult )
.then(
    displayAnswer,
    reportError
);

// Later
pr.ignored = true; // just hacking around

换句话说,你为了可以取消你的promise,在promise上面加了一层来处理这种状况。你不能从promise取消注册处理函数。而且由于一个promise必须不可变,你可以直接取消一个promise这种状况是不容许出现的。从外部取消一个promise跟改变它的状态没有什么区别。它使得promise变得不可靠。

许多promise库都提供了这种功能,可是这明显是一个错误。取消这种行为是不须要promise,可是它能够出如今promise上面的一个抽象层里。

冗长

另外一个关于原生promise的担忧是有些事情并无被实现,因此你必须自动手动实现它们,而这些事情对于可扩展性是很重要的,可是这些东西常常会致使使人讨厌的重复代码。

看一个例子,在每个promise的完成步骤中,有一个设定就是你但愿保持链式结构,因此then(..)方法会返回一个新的promise。可是若是你想要加入一个本身建立的promise而且从一个成功处理函数中返回,这样你的promise就能够加入到链的流程控制中。

function transformResult(data) {
    // we have to manually create and return a promise here
    return new Promise( function(resolve,reject){
        // whatever
    } );
}

var pr = ajax( ".." )
.then( transformResult )
.then(
    displayAnswer,
    reportError
);

不一样的是,就像上面解释的同样,从第一个then(..)返回的隐藏的promise马上就完成(或者失败),而后你就没办法让剩下的链异步延迟。若是有一个抽象层可以经过某种方式把自动建立/连接的promise暴露给你,而后你就不须要建立本身的promise来替换了,这样该多好。

换句话说,若是有一个设定假设你须要为了异步的目的使用链,而不是你只是须要漂亮得执行异步。(也就是说你确实是但愿你的代码能够异步执行,而不是说但愿整个异步流程看过去好看点)。

另外一个例子:你不能直接传递一个已经存在的promise给then(..)方法,你必须传递一个返回这个promise的函数。

var pr = doTask2();

doTask1()
.then( pr ); // would be nice, but doesn't work!

// instead:

doTask1()
.then( function(){ return pr; } );

这个限制性是有不少缘由的。可是它只是减弱了有利于保持可扩展性和可预测性的用法的简洁。抽象能够容易的解决这个问题。

全剧终

全部这些缘由就是为何原生的promise API是强大同时也是有局限性的。

关于扩展和抽象是一个成熟的领域。许多库正在作这些工做。就像我以前说的,asynquence是我本身的promise抽象库。它很小可是很强大。它解决了全部博客中提到的promise的问题。

我后面会写一篇详细的博客来介绍asynquence是若是解决这些问题的,因此敬请期待。

深刻理解Promise五部曲--1.异步问题
深刻理解Promise五部曲--2.转换问题
深刻理解Promise五部曲--3.可靠性问题
深刻理解Promise五部曲--4.扩展性问题
深刻理解Promise五部曲--5.乐高问题

最后,安利下个人我的博客,欢迎访问:http://bin-playground.top

相关文章
相关标签/搜索