«Поэтапное» выполнение функций в javascript - PullRequest
5 голосов
/ 31 марта 2010

Это мой первый пост о stackoverflow, поэтому, пожалуйста, не расстраивайте меня слишком сильно, если я сталкиваюсь с полным беспорядком или если я не могу сделать себя совершенно ясно. : -)

Вот моя проблема: я пытаюсь написать функцию javascript, которая «связывает» две функции с другой, проверяя завершение первой, а затем выполняя вторую.

Очевидно, что простым решением этой проблемы было бы написание мета-функции, которая вызывает обе функции в своей области видимости. Однако, если первая функция асинхронная (в частности, вызов AJAX), а вторая функция требует данных результата первой, это просто не будет работать.

Моя идея для решения состояла в том, чтобы дать первой функции «флаг», то есть заставить ее создать открытое свойство «this.trigger» (инициализируется как «0», после завершения становится равным «1»), как только она вызывается ; это должно позволить другой функции проверить флаг на его значение ([0,1]). Если условие выполнено («trigger == 1»), вторая функция должна быть вызвана.

Ниже приведен абстрактный пример кода, который я использовал для тестирования:

<script type="text/javascript" >

/**/function cllFnc(tgt) { //!! first function

    this.trigger = 0 ;
    var trigger = this.trigger ;

    var _tgt = document.getElementById(tgt) ; //!! changes the color of the target div to signalize the function's execution
        _tgt.style.background = '#66f' ;

    alert('Calling! ...') ;

    setTimeout(function() { //!! in place of an AJAX call, duration 5000ms

            trigger = 1 ;

    },5000) ;

}

/**/function rcvFnc(tgt) { //!! second function that should get called upon the first function's completion

    var _tgt = document.getElementById(tgt) ; //!! changes color of the target div to signalize the function's execution
        _tgt.style.background = '#f63' ;

    alert('... Someone picked up!') ;

}

/**/function callCheck(obj) {   

            //alert(obj.trigger ) ;      //!! correctly returns initial "0"                         

    if(obj.trigger == 1) {              //!! here's the problem: trigger never receives change from function on success and thus function two never fires 

                        alert('trigger is one') ;
                        return true ;
                    } else if(obj.trigger == 0) {
                        return false ;
                    }


}

/**/function tieExc(fncA,fncB,prms) {

        if(fncA == 'cllFnc') {
            var objA = new cllFnc(prms) ;   
            alert(typeof objA + '\n' + objA.trigger) ;  //!! returns expected values "object" and "0"
        } 

        //room for more case definitions

    var myItv = window.setInterval(function() {

        document.getElementById(prms).innerHTML = new Date() ; //!! displays date in target div to signalize the interval increments


        var myCallCheck = new callCheck(objA) ; 

            if( myCallCheck == true ) { 

                    if(fncB == 'rcvFnc') {
                        var objB = new rcvFnc(prms) ;
                    }

                    //room for more case definitions

                    window.clearInterval(myItv) ;

            } else if( myCallCheck == false ) {
                return ;
            }

    },500) ;

}

</script>

HTML-часть для тестирования:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/strict.dtd >

<html>

<head>

    <script type="text/javascript" >
       <!-- see above -->
    </script>

    <title>

      Test page

    </title>


</head>

<body>

    <!-- !! testing area -->

        <div id='target' style='float:left ; height:6em ; width:8em ; padding:0.1em 0 0 0; font-size:5em ; text-align:center ; font-weight:bold ; color:#eee ; background:#fff;border:0.1em solid #555 ; -webkit-border-radius:0.5em ;' >
            Test Div
        </div>

        <div style="float:left;" >
            <input type="button" value="tie calls" onmousedown="tieExc('cllFnc','rcvFnc','target') ;" />
        </div>

<body>


</html>

Я почти уверен, что это какая-то проблема с областью JavaScript, так как я проверил, правильно ли установлен триггер на «1», и это так. Очень вероятно, что функция «checkCall ()» не получает обновленный объект, а вместо этого только проверяет его старый экземпляр, который, очевидно, никогда не отмечает завершение, устанавливая «this.trigger» в «1». Если это так, я не знаю, как решить эту проблему.

В любом случае, надеюсь, что у кого-то есть идея или опыт решения этой конкретной проблемы.

Спасибо за чтение!

FK

Ответы [ 5 ]

8 голосов
/ 31 марта 2010

Вы можете воспользоваться функцией JS, называемой закрытием. Объедините это с очень распространенным шаблоном JS, называемым «стиль передачи продолжения», и вы получите свое решение. (Ни одна из этих вещей не является оригинальной для JS, но интенсивно используется в JS).

// a function
function foo(some_input_for_foo, callback)
{
    // do some stuff to get results

    callback(results); // call our callback when finished
}

// same again
function bar(some_input_for_bar, callback)
{
    // do some stuff to get results

    callback(results); // call our callback when finished
}

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

Вы можете легко связать их вместе:

foo(input1, function(results1) {

    bar(results1, function(results2) {

        alert(results2);
    });
});

Вложенные анонимные функции могут "видеть" переменные из области видимости, в которой они живут. Поэтому нет необходимости использовать специальные свойства для передачи информации.

Обновление

Чтобы уточнить, во фрагменте кода вашего вопроса ясно, что вы думаете примерно так:

у меня длительный асинхронный операция, поэтому мне нужно знать, когда это заканчивается, чтобы начать следующий операция. Так что мне нужно сделать это состояние видно как свойство. затем в другом месте я могу бегать по кругу, неоднократно исследуя это свойство посмотрим, когда он изменится на «выполнено» состояние, поэтому я знаю, когда продолжить.

(И, кроме того, в качестве усложняющего фактора цикл должен использовать setInterval для запуска и clearInterval для выхода, чтобы позволить запускать другой код JS - но, тем не менее, это в основном "цикл опроса").

Вам не нужно этого делать!

Вместо того, чтобы заставлять вашу первую функцию устанавливать свойство по завершении, заставьте ее вызывать функцию.

Чтобы сделать это абсолютно понятным, давайте проведем рефакторинг вашего исходного кода:

function cllFnc(tgt) { //!! first function

    this.trigger = 0 ;
    var trigger = this.trigger ;

    var _tgt = document.getElementById(tgt) ; //!! changes the color...
    _tgt.style.background = '#66f' ;

    alert('Calling! ...') ;

    setTimeout(function() { //!! in place of an AJAX call, duration 5000ms

        trigger = 1 ;

    },5000) ;
}

[ Обновление 2 : Кстати, там есть ошибка. Вы копируете текущее значение свойства trigger в новую локальную переменную с именем trigger. Затем в конце вы назначаете 1 этой локальной переменной. Никто другой не сможет увидеть это. Локальные переменные являются частными для функции. Но вам все равно ничего не нужно делать, так что продолжайте читать ... ]

Все, что нам нужно сделать, это сказать этой функции, что вызывать, когда она будет выполнена, и избавиться от установки свойства:

function cllFnc(tgt, finishedFunction) { //!! first function

    var _tgt = document.getElementById(tgt) ; //!! changes the color...
    _tgt.style.background = '#66f' ;

    alert('Calling! ...') ;

    setTimeout(function() { //!! in place of an AJAX call, duration 5000ms

        finishedFunction(); // <-------- call function instead of set property

    },5000) ;
}

Теперь нет необходимости в "проверке вызовов" или специальном помощнике tieExc. Вы можете легко связать две функции вместе с очень небольшим кодом.

var mySpan = "#myspan";

cllFnc(mySpan, function() { rcvFnc(mySpan); });

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

Например, первая функция может выполнить пару вызовов службы AJAX (для краткости используется jQuery):

function getCustomerBillAmount(name, callback) {

    $.get("/ajax/getCustomerIdByName/" + name, function(id) {

        $.get("/ajax/getCustomerBillAmountById/" + id), callback);

    });
}

Здесь callback принимает сумму счета клиента, а вызов AJAX get передает полученное значение функции, которую мы передаем ему, поэтому callback уже совместим и поэтому может напрямую выступать в качестве обратного вызова для второй вызов AJAX. Так что это сам пример объединения двух асинхронных вызовов в последовательности и их оборачивания в то, что кажется (снаружи) единой асинхронной функцией.

Тогда мы можем связать это с другой операцией:

function displayBillAmount(amount) {

    $("#billAmount").text(amount); 
}

getCustomerBillAmount("Simpson, Homer J.", displayBillAmount);

Или мы могли бы (снова) использовать анонимную функцию:

getCustomerBillAmount("Simpson, Homer J.", function(amount) {

    $("#billAmount").text(amount); 
});

Таким образом, объединяя вызовы функций таким образом, каждый шаг может передавать информацию на следующий шаг, как только она становится доступной.

Заставив функции выполнять обратный вызов по завершении, вы освобождаетесь от любых ограничений на то, как каждая функция работает внутри. Он может делать вызовы AJAX, таймеры, что угодно. Пока обратный вызов «продолжения» передается вперед, может быть любое количество уровней асинхронной работы.

По сути, в асинхронной системе, если вы когда-нибудь будете писать цикл для проверки переменной и выяснения, изменилось ли ее состояние, то что-то пошло не так. Вместо этого должен быть способ предоставления функции, которая будет вызываться при изменении состояния.

Обновление 3

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

Обновление 4

Совсем недавно я написал короткое сообщение в блоге на тему кэширования результатов асинхронных вызовов в JavaScript .

(конец обновления 4)

Еще один способ поделиться результатами - предоставить возможность для одного обратного вызова для «трансляции» или «публикации» нескольким подписчикам:

function pubsub() {
    var subscribers = [];

    return {
        subscribe: function(s) {
            subscribers.push(s);
        },
        publish: function(arg1, arg2, arg3, arg4) {
            for (var n = 0; n < subscribers.length; n++) {
                subscribers[n](arg1, arg2, arg3, arg4);
            }
        }
    };
}

Итак:

finished = pubsub();

// subscribe as many times as you want:

finished.subscribe(function(msg) {
    alert(msg);
});

finished.subscribe(function(msg) {
    window.title = msg;
});

finished.subscribe(function(msg) {
    sendMail("admin@mysite.com", "finished", msg);
});

Тогда пусть какая-нибудь медленная операция опубликует свои результаты:

lookupTaxRecords("Homer J. Simpson", finished.publish);

Когда этот звонок завершится, он будет звонить всем трем абонентам.

5 голосов
/ 31 марта 2010

Окончательным ответом на эту проблему «позвони мне, когда будешь готов» является обратный вызов . Обратный вызов - это, по сути, функция, которую вы назначаете свойству объекта (например, «onload»). Когда состояние объекта изменяется, вызывается функция. Например, эта функция отправляет ajax-запрос к указанному URL-адресу и выдает сообщение о его завершении:

function ajax(url) {
    var req = new XMLHttpRequest();  
    req.open('GET', url, true);  
    req.onreadystatechange = function (aEvt) {  
        if(req.readyState == 4)
            alert("Ready!")
    }
    req.send(null);  
}

Конечно, это недостаточно гибко, потому что мы, вероятно, хотим разные действия для разных вызовов ajax. К счастью, javascript - это функциональный язык, поэтому мы можем просто передать требуемое действие в качестве параметра:

function ajax(url, action) {
    var req = new XMLHttpRequest();  
    req.open('GET', url, true);  
    req.onreadystatechange = function (aEvt) {  
        if(req.readyState == 4)
            action(req.responseText);
    }
    req.send(null);  
}

Эта вторая функция может использоваться следующим образом:

 ajax("http://...", function(text) {
      do something with ajax response  
 });

Согласно комментариям, вот пример того, как использовать ajax в объекте

function someObj() 
{
    this.someVar = 1234;

    this.ajaxCall = function(url) {
        var req = new XMLHttpRequest();  
        req.open('GET', url, true);  

        var me = this; // <-- "close" this

        req.onreadystatechange = function () {  
            if(req.readyState == 4) {
                // save data...
                me.data = req.responseText;     
                // ...and/or process it right away
                me.process(req.responseText);   

            }
        }
        req.send(null);  
    }

    this.process = function(data) {
        alert(this.someVar); // we didn't lost the context
        alert(data);         // and we've got data!
    }
}


o = new someObj;
o.ajaxCall("http://....");

Идея состоит в том, чтобы "закрыть" (с псевдонимом) "this" в обработчике событий, чтобы его можно было передавать дальше.

1 голос
/ 01 апреля 2010

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

Обновление

Пробовал код в Webkit (Safari, Chrome), Mozilla и Opera. Кажется, работает просто отлично. С нетерпением жду любых ответов.

Обновление 2

Я изменил метод tieExc (), чтобы интегрировать синтаксис вызова функции Anurag. Теперь вы можете вызывать столько функций, сколько хотите, после проверки завершения, передавая их в качестве аргументов.

Если вы не склонны читать код, попробуйте: http://jsfiddle.net/UMuj3/ (кстати, JSFiddle - действительно отличный сайт!).

JS-код:

/**/function meta() {

var myMeta = this ;

/**  **/this.cllFnc = function(tgt,lgt) { //!! first function

    this.trigger = 0 ;  //!! status flag, initially zero
    var that = this ;   //!! required to access parent scope from inside nested function

    var _tgt = document.getElementById(tgt) ; //!! changes the color of the target div to signalize the function's execution
    _tgt.style.background = '#66f' ;

    alert('Calling! ...') ;

    setTimeout(function() { //!! simulates longer AJAX call, duration 5000ms

        that.trigger = 1 ;  //!! status flag, one upon completion

    },5000) ;

} ;

/**  **/this.rcvFnc = function(tgt) { //!! second function that should get called upon the first function's completion

    var _tgt = document.getElementById(tgt) ; //!! changes color of the target div to signalize the function's execution
    _tgt.style.background = '#f63' ;

    alert('... Someone picked up!') ;

} ;

/**  **/this.callCheck = function(obj) {    

    return (obj.trigger == 1)   ?   true
        :   false
        ;

} ;

/**  **/this.tieExc = function() {

    var functions = arguments ;

    var myItv = window.setInterval(function() {

        document.getElementById('target').innerHTML = new Date() ; //!! displays date in target div to signalize the interval increments

        var myCallCheck = myMeta.callCheck(functions[0]) ; //!! checks property "trigger"

        if(myCallCheck == true) { 

            clearInterval(myItv) ;

            for(var n=1; n < functions.length; n++) {

                functions[n].call() ;

            }

        } else if(myCallCheck == false) { 
            return ;
        }

    },100) ;



} ;

}​

HTML

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/strict.dtd >

<html>

<head>

    <script type='text/javascript'  >
        <!-- see above -->
    </script>
    <title>

      Javascript Phased Execution Test Page

    </title>

</head>

<body>

        <div id='target' style='float:left ; height:7.5em ; width:10em ; padding:0.5em 0 0 0; font-size:4em ; text-align:center ; font-weight:bold ; color:#eee ; background:#fff;border:0.1em solid #555 ; -webkit-border-radius:0.5em ;' >
            Test Div
        </div>

        <div style="float:left;" >
            <input type="button" value="tieCalls()" onmousedown="var myMeta = new meta() ; var myCll = new myMeta.cllFnc('target') ; new myMeta.tieExc(myCll, function() { myMeta.rcvFnc('target') ; }, function() { alert('this is fun stuff!') ; } ) ;" /><br />
        </div>

<body>


</html>
1 голос
/ 31 марта 2010

Добро пожаловать на ТАК! Кстати, вы сталкиваетесь с полной глупостью, и ваш вопрос совершенно неясен:)

Это основывается на ответе Даниила об использовании продолжений. Это простая функция, которая объединяет несколько методов. Очень похоже на то, как труба | работает в Unix. В качестве аргументов он принимает набор функций, которые должны выполняться последовательно. Возвращаемое значение каждого вызова функции передается следующей функции в качестве параметра.

function Chain() {
    var functions = arguments;

    return function(seed) {
        var result = seed;

        for(var i = 0; i < functions.length; i++) {
            result = functions[i](result);
        }

        return result;
    }
}

Чтобы использовать его, создайте объект из Chained, передав все функции в качестве параметров. Пример, который вы можете проверить на скрипке , будет:

​var chained = new Chain(
    function(a) { return a + " wo"; },
    function(a) { return a + "r"; },
    function(a) { return a + "ld!"; }
);

alert(chained('hello')); // hello world!

Чтобы использовать его с запросом AJAX, передайте цепочечную функцию в качестве обратного вызова успеха XMLHttpRequest.

​var callback = new Chain(
    function(response) { /* do something with ajax response */ },
    function(data) { /* do something with filtered ajax data */ }
);

var req = new XMLHttpRequest();  
req.open('GET', url, true);  
req.onreadystatechange = function (aEvt) {  
    if(req.readyState == 4)
        callback(req.responseText);
}
req.send(null);  

Важно то, что каждая функция зависит от вывода предыдущей функции, поэтому вы должны возвращать некоторое значение на каждом этапе.

<ч />

Это всего лишь предложение - возложение ответственности за проверку наличия данных на локальном уровне или выполнение HTTP-запроса увеличит сложность системы. Вместо этого у вас может быть непрозрачный менеджер запросов, очень похожий на metaFunction, который у вас есть, и он может решить, будут ли данные обслуживаться локально или удаленно.

Вот образец Request объекта , который обрабатывает эту ситуацию без каких-либо других объектов или функций, зная, откуда были получены данные:

var Request = {
    cache: {},

    get: function(url, callback) {
        // serve from cache, if available
        if(this.cache[url]) {
            console.log('Cache');
            callback(this.cache[url]);
            return;
        }
        // make http request
        var request = new XMLHttpRequest();
        request.open('GET', url, true);
        var self = this;
        request.onreadystatechange = function(event) {
            if(request.readyState == 4) {
                self.cache[url] = request.responseText;
                console.log('HTTP');
                callback(request.responseText);
            }
        };
        request.send(null);
    }
};

Чтобы использовать его, вы должны позвонить на Request.get(..), и он вернет кэшированные данные, если они доступны, или сделает AJAX-вызов в противном случае. Третий параметр может быть передан для контроля продолжительности кэширования данных, если вы ищете детальный контроль над кэшированием.

Request.get('<url>', function(response) { .. }); // HTTP
// assuming the first call has returned by now
Request.get('<url>', function(response) { .. }); // Cache
Request.get('<url>', function(response) { .. }); // Cache
0 голосов
/ 31 марта 2010

Очень простым решением было бы сделать ваш первый вызов ajax синхронным. Это один из необязательных параметров.

...