异步顺序调用方法

Posted

技术标签:

【中文标题】异步顺序调用方法【英文标题】:sequential calls of methods asynchronously 【发布时间】:2012-03-15 01:53:25 【问题描述】:

我有一个我在一个方法中调用的方法列表,如下:

this.doOneThing();
someOtherObject.doASecondThing();
this.doSomethingElse();

当这是同步时,它们一个接一个地执行,这是必需的。但是现在我有 someOtherObject.doASecondThing() 作为异步,我也可能把 doOneThing 变成异步的。我可以使用回调并从回调内部调用 that.doSomethingElse:

var that = this;
this.doOneThing( function ()  
                    someOtherObject.doASecondThing(function () 
                        that.doSomethingElse();
                    );
                  );

但是,由于序列在增长,让回调相互调用似乎有点混乱,由于某种原因,它使序列看起来不像以前那么明显,并且缩进可能会随着调用的方法数量而增长顺序。

有没有办法让它看起来更好?我也可以使用观察者模式,但在我看来,它也不会让事情变得很明显。

谢谢,

【问题讨论】:

也许this 看起来很有趣。 这个问题被称为“回调意大利面条”。 如果要求它们是同步的,即每个“任务”都依赖于另一个。然后要切换到异步,您必须删除依赖项。通过将任务分解为小步骤,您可能会取得一些进展,但如果不是异步的,则会浪费资源并显着增加复杂性。 嗨,其中一些是异步的。 Q 模块看起来很有趣,但我不确定是否要添加另一个库。我得考虑一下。谢谢, 您可能想研究“Promise”架构。 【参考方案1】:

延续,以及它们为什么会导致回调意大利面条

在回调中编写会迫使您在某个时候以类似于“连续传递样式”(CPS) 的方式编写代码,这是一种极其强大但困难的技术。它代表了控制的完全反转,实际上是“颠倒”了计算。 CPS 使您的代码结构明确地反映程序的控制流(有时是好事,有时是坏事)。实际上,您是在显式写下匿名函数的堆栈。

作为理解此答案的先决条件,您可能会发现这很有用:

http://matt.might.net/articles/by-example-continuation-passing-style/

例如,这是你正在做的:

function thrice(x, ret) 
    ret(x*3)

function twice(y, ret) 
    ret(y*2)

function plus(x,y, ret) 
    ret(x+y)


function threeXPlusTwoY(x,y, ret) 
    // STEP#1
    thrice(x,                 // Take the result of thrice(x)...
        function(r1)         // ...and call that r1.
            // STEP#2
            twice(y,            // Take the result of twice(y)...
                function(r2)   // ...and call that r2.
                    // STEP#3
                    plus(r1,r2,   // Take r1+r2...
                        ret       // ...then do what we were going to do.
                    )
                
            )
        
    )


threeXPlusTwoY(5,1, alert);  //17

正如您所抱怨的,这会导致代码缩进,因为闭包是捕获此堆栈的自然方式。


单子救援

取消缩进 CPS 的一种方法是像在 Haskell 中那样“单调地”编写。我们将如何做到这一点?在 javascript 中实现 monad 的一种好方法是使用点链表示法,类似于 jQuery。 (请参阅http://importantshock.wordpress.com/2009/01/18/jquery-is-a-monad/ 以获得有趣的转移。)或者我们可以使用反射。

但首先我们需要一种“写下管道”的方法,然后我们可以找到一种方法将其抽象出来。可悲的是,在 javascript 中编写通用的 monad 语法有点困难,所以我将使用列表来表示计算。

 

// switching this up a bit:
// it's now 3x+2x so we have a diamond-shaped dependency graph

// OUR NEW CODE
var _x = 0;
var steps = [
    [0,  function(ret)ret(5),[]],  //step0:
    [1,  thrice,[_x]],               //step1: thrice(x)
    [2,  twice,[_x]],                //step2: twice(x)
    [3,  plus,[1, 2]]                //step3: steps[1]+steps[2] *
]
threeXPlusTwoX = generateComputation(steps);

//*this may be left ambiguous, but in this case we will choose steps1 then step2
// via the order in the array

这有点难看。但是我们可以让这个 UNINDENTED “代码” 工作。我们可以担心稍后让它更漂亮(在最后一节中)。在这里,我们的目的是写下所有“必要的信息”。我们想要一种简单的方法来编写每个“行”,以及我们可以在其中编写它们的上下文。

现在我们实现一个generateComputation,它会生成一些嵌套的匿名函数,如果我们执行它,这些函数将按顺序执行上述步骤。这是这样一个实现的样子:

function generateComputation(steps) 
    /*
    * Convert steps object into a function(ret), 
    * which when called will perform the steps in order.
    * This function will call ret(_) on the results of the last step.
    */
    function computation(ret) 
        var stepResults = [];

        var nestedFunctions = steps.reduceRight(
            function(laterFuture, step) 
                var i            = step[0];  // e.g. step #3
                var stepFunction = step[1];  // e.g. func: plus
                var stepArgs     = step[2];  // e.g. args: 1,2

                console.log(i, laterFuture);
                return function(returned) 
                    if (i>0)
                        stepResults.push(returned);
                    var evalledStepArgs = stepArgs.map(function(s)return stepResults[s]);
                    console.log(i:i, returned:returned, stepResults:stepResults, evalledStepArgs:evalledStepArgs, stepFunction:stepFunction);
                    stepFunction.apply(this, evalledStepArgs.concat(laterFuture));
                
            ,
            ret
        );

        nestedFunctions();
    
    return computation;

演示:

threeXPlusTwoX = generateComputation(steps)(alert);  // alerts 25

旁注:reduceRight 语义意味着右边的步骤将更深入地嵌套在函数中(将来会更深)。供不熟悉的人参考,[1,2,3].reduce(f(_,_), x) --> f(f(f(0,1), 2), 3)reduceRight(由于糟糕的设计考虑)实际上等同于 [1.2.3].reversed().reduce(...)

在上面,generateComputation 制作了一堆嵌套函数,将它们相互包装以备准备,当使用 ...(alert) 评估时,将它们一个接一个地拆开以供计算。

旁注:我们必须使用 hack,因为在前面的示例中,我们使用闭包和变量名来实现 CPS。 Javascript 不允许足够的反射来执行此操作,而无需借助制作字符串和evaling(ick),因此我们暂时避开函数式样式并选择更改跟踪所有参数的对象。因此,上述内容更接近于以下内容:

var x = 5;
function _x(ret) 
    ret(x);


function thrice(x, ret) 
    ret(x*3)

function twice(y, ret) 
    ret(y*2)

function plus(x,y, ret) 
    ret(x+y)


function threeXPlusTwoY(x,y, ret) 
    results = []
    _x(
        return function(x) 
            results[0] = x;

            thrice(x,                 // Take the result of thrice(x)...
                function(r1)         // ...and call that r1.
                    results[1] = r1;

                    twice(y,            // Take the result of twice(y)...
                        function(r2)   // ...and call that r2.
                            results[2] = r2;

                            plus(results[1],results[2],   // Take r1+r2...
                                ret       // ...then do what we were going to do.
                            )
                        
                    )
                
            )

        
    )


理想语法

但我们仍然希望以一种理智的方式编写函数。我们理想情况下希望如何编写代码以利用 CPS,同时保持我们的理智?文献中有很多内容(例如,Scala 的 shiftreset 运算符只是众多方法之一),但为了理智起见,让我们找到一种方法来为常规 CPS 制作语法糖。有一些可能的方法:

 

// "bad"
var _x = 0;
var steps = [
    [0,  function(ret)ret(5),[]],  //step0:
    [1,  thrice,[_x]],               //step1: thrice(x)
    [2,  twice,[_x]],                //step2: twice(x)
    [3,  plus,[1, 2]]                //step3: steps[1]+steps[2] *
]
threeXPlusTwoX = generateComputation(steps);

...变成...

如果回调在一个链中,我们可以轻松地将一个输入到下一个,而无需担心命名。这些函数只有一个参数:回调参数。 (如果没有,你可以在最后一行将函数 curry 如下。)这里我们可以使用 jQuery 样式的点链。

 

// SYNTAX WITH A SIMPLE CHAIN
// ((2*X) + 2)
twiceXPlusTwo = callbackChain()
    .then(prompt)
    .then(twice)
    .then(function(returned)return plus(returned,2));  //curried

twiceXPlusTwo(alert);

如果回调形成依赖树,我们也可以使用 jQuery 样式的点链,但这会破坏为 CPS 创建一元语法的目的,即扁平化嵌套函数。因此这里不再赘述。

如果回调形成依赖无环图(例如,2*x+3*x,其中 x 使用了两次),我们需要一种方法来命名某些回调的中间结果。这就是有趣的地方。我们的目标是尝试模仿http://en.wikibooks.org/wiki/Haskell/Continuation_passing_style 的语法及其do-notation,它将函数“解包”和“重新包装”进出CPS。不幸的是,[1, thrice,[_x]] 语法是我们可以轻松达到的最接近的语法(甚至没有接近)。您可以用另一种语言编写代码并编译为 javascript,或者使用 eval(排队不祥的音乐)。有点矫枉过正。替代方案必须使用字符串,例如:

 

// SUPER-NICE SYNTAX
// (3X + 2X)
thriceXPlusTwiceX = CPS(
    leftPart: thrice('x'),
    rightPart: twice('x'),
    result: plus('leftPart', 'rightPart')
)

只需对我描述的generateComputation 进行一些调整,您就可以做到这一点。首先,您将其调整为使用逻辑名称('leftPart' 等)而不是数字。然后你让你的函数实际上是惰性对象,其行为如下:

thrice(x).toListForm() == [<real thrice function>, ['x']]
or
thrice(x).toCPS()(5, alert)  // alerts 15
or
thrice.toNonCPS()(5) == 15

(您可以使用某种装饰器以自动方式执行此操作,而不是手动执行此操作。)

旁注:所有回调函数都应该遵循关于回调参数所在位置的相同协议。例如,如果您的函数以 myFunction(callback, arg0, arg1, ...)myFunction(arg0, arg1, ..., callback) 开头,它们可能并不完全兼容,但如果它们不兼容,您可以执行 javascript 反射黑客来查看函数的源代码并将其正则表达式输出,并且因此不必担心。

为什么要经历这么多麻烦?这使您可以混合setTimeouts 和prompts 和ajax 请求,而不会遭受“缩进地狱”的困扰。您还可以获得一大堆其他好处(比如能够编写一个 10 行非确定性搜索数独求解器,并实现任意控制流运算符),我不会在这里介绍。

【讨论】:

以上是关于异步顺序调用方法的主要内容,如果未能解决你的问题,请参考以下文章

spring使用@Async注解异步处理

OC 异步顺序加载的方法

从另一个异步方法调用的 Spring 异步方法

C# 使用多个异步方法

Angular 2调用多个异步方法

多个嵌套异步 AJAX 调用和返回数据的顺序