последовательные вызовы методов асинхронно - PullRequest
3 голосов
/ 24 февраля 2012

У меня есть список методов, которые я вызываю в методе, следующим образом:

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

Когда это синхронно, они выполняются один за другим, что необходимо. Но теперь у меня есть someOtherObject.doASecondThing () как асинхронный, и я мог бы также сделать doOneThing как асинхронный. Я мог бы использовать обратный вызов и вызвать that.doSomethingElse из обратного вызова:

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

Однако, поскольку последовательность растет, кажется, что вызовы, вызывающие друг друга, выглядят немного беспорядочно, по какой-то причине последовательность выглядит не так очевидно, как раньше, и отступ может увеличиваться с увеличением числа методов, вызываемых последовательность.

Есть ли способ сделать это лучше? Я мог бы также использовать паттерн наблюдателя, но, на мой взгляд, это не делает вещи слишком очевидными.

Спасибо

1 Ответ

2 голосов
/ 25 февраля 2012

Продолжения и почему они вызывают спагетти обратного вызова

Запись в обратных вызовах вынуждает вас иногда писать сродни "стилю передачи продолжения" (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

sidenote: reduceRight семантика подразумевает, что шаги справа будут более глубоко вложеннымив функции (далее в будущем).К вашему сведению, для тех, кто не знаком, [1,2,3].reduce(f(_,_), x) --> f(f(f(0,1), 2), 3) и reduceRight (из-за плохих конструктивных соображений) фактически эквивалентны [1.2.3].reversed().reduce(...)

Выше, generateComputation сделал кучу вложенных функций, оборачиваяони друг в друге в процессе подготовки, и при оценке с помощью ...(alert), убирали их один за другим для подачи в вычисления.

sidenote: Мы должны использовать хак, потому что в предыдущем примереМы использовали замыкания и имена переменных для реализации CPS.Javascript не позволяет достаточно размышлений, чтобы сделать это, не прибегая к созданию строки и eval ее (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 shift и reset являются лишь одним из многих способов сделать это), но ради здравомыслия, давайте просто найдем способ сделать синтаксический сахар для обычного 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

(Вы можете сделать это автоматически с помощью какого-то декоратора, а не вручную.)

sidenote: Все вашиФункции обратного вызова должны следовать тому же протоколу о том, где находится параметр обратного вызова.Например, если ваши функции начинаются с myFunction(callback, arg0, arg1, ...) или myFunction(arg0, arg1, ..., callback), они могут быть тривиально несовместимы, хотя, если они не являются предположительно, вы могли бы сделать хак с отражением javascript, чтобы посмотреть на исходный код функции и вывести его на регулярное выражение, итаким образом, не нужно беспокоиться об этом.

Зачем проходить через все эти неприятности?Это позволяет смешивать запросы setTimeout s и prompt s и ajax, не страдая от "адского отступа".Вы также получите целый ряд других преимуществ (например, возможность написать 10-строчный решатель по недетерминированному поиску судоку и реализацию произвольных операторов потока управления), которые я здесь не буду рассматривать.

...