Javascript - синхронизация после асинхронных вызовов - PullRequest
20 голосов
/ 13 мая 2009

У меня есть объект Javascript, который требует 2 вызова внешнего сервера для создания его содержимого и выполнения каких-либо значимых действий. Объект построен так, что создание экземпляра его автоматически сделает эти 2 вызова. Эти 2 вызова имеют общую функцию обратного вызова, которая работает с возвращенными данными, а затем вызывает другой метод. Проблема в том, что следующий метод не должен вызываться до тех пор, пока оба метода не вернутся. Вот код, который я реализовал в настоящее время:

foo.bar.Object = function() {
this.currentCallbacks = 0;
this.expectedCallbacks = 2;

this.function1 = function() {
    // do stuff
    var me = this;
    foo.bar.sendRequest(new RequestObject, function(resp) {
        me.commonCallback(resp);
        });
};

this.function2 = function() {
    // do stuff
    var me = this;
    foo.bar.sendRequest(new RequestObject, function(resp) {
        me.commonCallback(resp);
        });
};

this.commonCallback = function(resp) {
    this.currentCallbacks++;
    // do stuff
    if (this.currentCallbacks == this.expectedCallbacks) {
        // call new method
    }
};

this.function1();
this.function2();
}

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

Спасибо за любую помощь.

Ответы [ 6 ]

13 голосов
/ 13 мая 2009

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

Вот функция, которая может вам помочь:

function gate(fn, number_of_calls_before_opening) {
    return function() {
        arguments.callee._call_count = (arguments.callee._call_count || 0) + 1;
        if (arguments.callee._call_count >= number_of_calls_before_opening)
            fn.apply(null, arguments);
    };
}

Эта функция называется функцией высшего порядка - функцией, принимающей функции в качестве аргументов. Эта конкретная функция возвращает функцию, которая вызывает переданную функцию, когда она была вызвана number_of_calls_before_opening раз. Например:

var f = gate(function(arg) { alert(arg); }, 2);
f('hello');
f('world'); // An alert will popup for this call.

Вы можете использовать это как метод обратного вызова:

foo.bar = function() {
    var callback = gate(this.method, 2);
    sendAjax(new Request(), callback);
    sendAjax(new Request(), callback);
}

Второй обратный вызов, в зависимости от того, что будет, вызовет method. Но это приводит к другой проблеме: функция gate вызывает переданную функцию без какого-либо контекста, то есть this будет ссылаться на глобальный объект, а не на объект, который вы создаете. Есть несколько способов обойти это: вы можете закрыть this, присвоив ему псевдоним me или self. Или вы можете создать другую функцию более высокого порядка, которая сделает именно это.

Вот как будет выглядеть первый случай:

foo.bar = function() {
    var me = this;        
    var callback = gate(function(a,b,c) { me.method(a,b,c); }, 2);

    sendAjax(new Request(), callback);
    sendAjax(new Request(), callback);
}

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

function bind_context(context, fn) {
    return function() {
        return fn.apply(context, arguments);
    };
}

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

var obj = {};
var func = function(name) { this.name = name; };
var method = bind_context(obj, func);
method('Your Name!');
alert(obj.name); // Your Name!

Чтобы представить это в перспективе, ваш код будет выглядеть следующим образом:

foo.bar = function() {
    var callback = gate(bind_context(this, this.method), 2);

    sendAjax(new Request(), callback);
    sendAjax(new Request(), callback);
}

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

8 голосов
/ 20 декабря 2011

Я могу добавить, что Underscore.js имеет хороший маленький помощник для этого :

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

_.after(count, function)

Код _after (начиная с версии 1.5.0):

_.after = function(times, func) {
  return function() {
    if (--times < 1) {
      return func.apply(this, arguments);
    }
  };
};

Информация о лицензии (начиная с версии 1.5.0)

5 голосов
/ 13 мая 2009

Существует только другой способ, чем иметь этот счетчик. Другим вариантом будет использование объекта {}, добавление ключа для каждого запроса и удаление его по завершении. Таким образом, вы бы сразу узнали, кто вернулся. Но решение остается прежним.

Вы можете немного изменить код. Если в вашем примере вам нужно только вызвать другую функцию внутри commonCallback (я назвал ее otherFunction), то вам не нужен commonCallback. Чтобы сохранить контекст, вы уже использовали замыкания. Вместо

foo.bar.sendRequest(new RequestObject, function(resp) {
            me.commonCallback(resp);
            });

Вы могли бы сделать это так

foo.bar.sendRequest(new RequestObject, function(resp) {
            --me.expectedCallbacks || me.otherFunction(resp);
            });
2 голосов
/ 12 апреля 2012

Хорошие вещи, мистер Кайл.

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

Объявления выглядят так.

var PendingRequests = 0;
function Start(Requests) {
    PendingRequests = Requests.length;
    for (var i = 0; i < Requests.length; i++)
        Requests[i]();
};
//Called when async responses complete. 
function Done(CompletedEvents) {
PendingRequests--;
    if (PendingRequests == 0) {
        for (var i = 0; i < CompletedEvents.length; i++)
            CompletedEvents[i]();
    }
}

Вот простой пример использования API Google Maps.

//Variables
var originAddress = "*Some address/zip code here*"; //Location A
var formattedAddress; //Formatted address of Location B
var distance; //Distance between A and B
var location; //Location B

//This is the start function above. Passing an array of two functions defined below.
Start(new Array(GetPlaceDetails, GetDistances));


//This function makes a request to get detailed information on a place. 
//Then callsback with the **GetPlaceDetailsComplete** function
function GetPlaceDetails() {
    var request = {
        reference: location.reference //Google maps reference id
    };
    var PlacesService = new google.maps.places.PlacesService(Map);
    PlacesService.getDetails(request, GetPlaceDetailsComplete);
}

function GetPlaceDetailsComplete(place, status) {
    if (status == google.maps.places.PlacesServiceStatus.OK) {
        formattedAddress = place.formatted_address;
        Done(new Array(PrintDetails));
    }
}


function GetDistances() {
    distService = new google.maps.DistanceMatrixService();
    distService.getDistanceMatrix(
    {
        origins: originAddress, 
        destinations: [location.geometry.location], //Location contains lat and lng
        travelMode: google.maps.TravelMode.DRIVING,
        unitSystem: google.maps.UnitSystem.IMPERIAL,
        avoidHighways: false,
        avoidTolls: false
    }, GetDistancesComplete);
}
function GetDistancesComplete(results, status) {
    if (status == google.maps.DistanceMatrixStatus.OK) {
        distance = results[0].distance.text;
        Done(new Array(PrintDetails));
    }
}

function PrintDetails() {
    alert(*Whatever you feel like printing.*);
}

Итак, в двух словах, то, что мы здесь делаем, это
-Передача массива функций в Start function
-Функция Start вызывает функции в массиве и устанавливает число PendingRequests
- В обратных вызовах для наших ожидающих запросов мы вызываем функцию Done -Функция Done принимает массив функций
-Функция Done уменьшает счетчик PendingRequests
-Если их больше нет ожидающих запросов, мы вызываем функции, переданные функции Done

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

0 голосов
/ 04 апреля 2014

Я разделяю то же разочарование. Когда я связывал больше асинхронных вызовов, это стало адом обратного вызова. Итак, я придумал собственное решение. Я уверен, что есть похожие решения, но я хотел создать что-то очень простое и удобное в использовании. Asynq - это сценарий, который я написал для объединения асинхронных задач. Таким образом, чтобы запустить f2 после f1, вы можете сделать:

asynq.run (f1, f2)

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

0 голосов
/ 13 июня 2013

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

Здесь функция execute_jobs является точкой входа. это займет список данных для выполнения одновременно. Сначала задается количество ожидающих заданий размером list. Затем он установил таймер для проверки конечного состояния (число падает до 0). И, наконец, он отправляет работу для каждого из данных. Каждое задание уменьшает количество ожидаемых заданий на единицу.

Это будет выглядеть примерно так:

var g_numJobs = 0;

function async_task(data) {
    //
    // ... execute the task on the data ...
    //

    // Decrease the number of jobs left to execute.
    --g_numJobs;
}

function execute_jobs(list) {
    // Set the number of jobs we want to wait for.
    g_numJobs = list.length;

    // Set the timer (test every 50ms).
    var timer = setInterval(function() {
        if(g_numJobs == 0) {
            clearInterval(timer);
            do_next_action();
        }
    }, 50);

    // Send the jobs.
    for(var i = 0; i < list.length; ++i) {
        async_task(list[i]));
    }
}

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

...