异步顺序调用方法

3
我有一系列在一个方法中调用的方法,如下所示:
this.doOneThing();
someOtherObject.doASecondThing();
this.doSomethingElse();

当这是同步的时候,它们一个接一个地执行,这是必需的。但现在我把someOtherObject.doASecondThing()变成了异步的,而且我可能也会把doOneThing变成异步的。我可以使用回调函数,并从回调函数中调用that.doSomethingElse:

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

然而,由于序列正在增长,回调函数互相调用会显得有点混乱,出于某种原因,这使得序列不如之前明显,并且随着序列中被调用方法的数量增加,缩进可能会变得更长。
有没有办法让它看起来更好?我也可以使用观察者模式,但在我看来,它也不能很清晰地解决问题。
谢谢。

也许这个看起来很有趣。 - pimvdb
该问题被称为“回调地狱”。 - jholster
如果它们需要同步,即每个“任务”都依赖于其他任务,则要切换到异步,必须删除这些依赖关系。您可以通过将任务分解为小步骤来取得一些进展,但如果无法实现异步,则会浪费资源并显著增加复杂性。 - Tony Hopkinson
嗨,有些必须要异步。Q模块似乎非常有趣,但我不确定是否想要添加另一个库。我需要考虑一下。谢谢。 - Loic Duros
你可能想要了解一下“Promise”架构。 - Pointy
1个回答

2

Continuations,以及它们为什么会导致回调地狱

使用回调函数编写代码会迫使你使用类似于“传递续延”(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 中实现单子的一种不错的方法是使用点链接符号,类似于 jQuery。(有趣的是,参见http://importantshock.wordpress.com/2009/01/18/jquery-is-a-monad/。)或者我们可以使用反射。

但首先我们需要一种“写下管道”的方法,然后才能找到一种抽象它的方法。可悲的是,在 JavaScript 中编写通用的单子语法有些困难,因此我将使用列表来表示计算。

// 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

这有点丑陋,但我们可以让这个未缩进的“代码”工作。我们可以在最后一节中考虑如何使其更美观。我们的目的是记录所有“必要信息”的简便方式。我们希望有一种简单的方法来编写每个“行”,以及我们可以编写它们的上下文。

现在,我们实现一个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) 进行评估时,逐个取消包装以输入计算。

旁注:由于在前面的示例中,我们使用闭包和变量名称来实现 CPS。Javascript 不允许足够的反射来做到这一点,而不必诉诸于制作一个字符串并 eval 它(令人讨厌),因此我们暂时放弃函数式风格,选择突变一个对象来跟踪所有参数。因此,以上更接近以下内容:

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);

...变成...

  • 如果回调函数是链式的,我们可以轻松地将一个回调函数传递给下一个,而不用担心命名问题。这些函数只有一个参数:回调参数。(如果没有,你可以在最后一行上使用柯里化函数。) 在这里,我们可以使用 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 符号的语法,该符号“展开”和“重写”函数进入和退出 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反射hack来查看函数的源代码并将其正则匹配出来,从而无需担心。

为什么要费这么多事呢?这样可以混合使用setTimeoutprompt和ajax请求,而不会受到“缩进地狱”的困扰。您还可以获得许多其他好处(比如能够编写一个10行的非确定性搜索数独求解器,并实现任意控制流运算符),这里不再赘述。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接