Обработка взаимозависимых и/или многоуровневых асинхронных вызовов

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

var file_list = fetchFiles(source);

if (!file_list) {
    display('failed to fetch list');

} else {
        for (file in file_list) { // iteration, not enumeration
        var data = loadFile(file);

        if (!data) {
            display('failed to load: ' + file);
        } else {
            display(data);
        }
    }
}

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

Теперь, чтобы раздавить мои мечты: fetchFiles() и loadFile() на самом деле асинхронны. Легкий выход - превратить их в синхронные функции. Но это плохо, если браузер блокируется, ожидая завершения вызовов.

Как я могу обрабатывать множественные взаимозависимые и/или многоуровневые асинхронные вызовы, не углубляясь глубже и глубже в бесконечную цепочку обратных вызовов, в классическом режиме редукции ad spaghettum? Существует ли проверенная парадигма для их четкой обработки при сохранении слабого кода?

Ответ 1

Отсрочка - это действительно путь сюда. Они точно фиксируют то, что вы (и целый асинхронный код) хотите: "Уходите и делайте это потенциально дорого, не мешайте мне пока, а затем делайте это, когда вернетесь".

И вам не нужно использовать jQuery. Предприимчивый человек портировал Отложить на подчеркивание и утверждает, что вам даже не нужен символ подчеркивания, чтобы использовать его.

Итак, ваш код может выглядеть так:

function fetchFiles(source) {
    var dfd = _.Deferred();

    // do some kind of thing that takes a long time
    doExpensiveThingOne({
        source: source,
        complete: function(files) {
            // this informs the Deferred that it succeeded, and passes
            // `files` to all its success ("done") handlers
            dfd.resolve(files);

            // if you know how to capture an error condition, you can also
            // indicate that with dfd.reject(...)
        }
    });

    return dfd;
}

function loadFile(file) {
    // same thing!
    var dfd = _.Deferred();

    doExpensiveThingTwo({
        file: file,
        complete: function(data) {
            dfd.resolve(data);
        }
    });

    return dfd;
}

// and now glue it together
_.when(fetchFiles(source))
.done(function(files) {
    for (var file in files) {
        _.when(loadFile(file))
        .done(function(data) {
            display(data);
        })
        .fail(function() {
            display('failed to load: ' + file);
        });
    }
})
.fail(function() {
    display('failed to fetch list');
});

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

var file_dfds = [];
for (var file in files) {
    file_dfds.push(loadFile(file));
}

_.when(file_dfds)
.done(function(datas) {
    // this will only run if and when ALL the files have successfully
    // loaded!
});

Ответ 2

События

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

Я использовал bean в качестве рамки для событий.

Пример псевдокода:

// async request for files
function fetchFiles(source) {

    IO.get(..., function (data, status) {
        if(data) {
            bean.fire(window, 'fetched_files', data);
        } else {
            bean.fire(window, 'fetched_files_fail', data, status);
        } 
    });

}

// handler for when we get data
function onFetchedFiles (event, files) {
    for (file in files) { 
        var data = loadFile(file);

        if (!data) {
            display('failed to load: ' + file);
        } else {
            display(data);
        }
    }
}

// handler for failures
function onFetchedFilesFail (event, status) {
    display('Failed to fetch list. Reason: ' + status);
}

// subscribe the window to these events
bean.on(window, 'fetched_files', onFetchedFiles);
bean.on(window, 'fetched_files_fail', onFetchedFilesFail);

fetchFiles();

Пользовательские события и подобная обработка событий реализованы практически во всех популярных JS-инфраструктурах.

Ответ 3

Похоже, вам нужно jQuery Отложен. Вот какой-то непроверенный код, который может помочь вам в правильном направлении:

$.when(fetchFiles(source)).then(function(file_list) { 
  if (!file_list) {
    display('failed to fetch list');
  } else {
    for (file in file_list) {
      $.when(loadFile(file)).then(function(data){
        if (!data) {
          display('failed to load: ' + file);
        } else {
          display(data);
        }
      });
    }
  }
});

Я также нашел еще один достойный пост, который дает несколько случаев использования для объекта Отложенные

Ответ 4

Если вы не хотите использовать jQuery, то вместо этого вы можете использовать веб-работников в сочетании с синхронными запросами. Веб-рабочие поддерживаются в каждом главном браузере, за исключением любой версии Internet Explorer до 10.

совместимость браузера Web Worker

В принципе, если вы не совсем уверены в том, что такое веб-рабочий, подумайте об этом как о том, как браузеры могут выполнять специализированный JavaScript в отдельном потоке без влияния на основной поток (Caveat: на одноядерном CPU, оба потока будут чередоваться. К счастью, большинство компьютеров в настоящее время оснащены двухъядерными процессорами). Обычно веб-работники зарезервированы для сложных вычислений или некоторой интенсивной задачи обработки. Просто имейте в виду, что любой код внутри веб-пользователя НЕ МОЖЕТ ссылаться на DOM и не может ссылаться на любые глобальные структуры данных, которые не были переданы ему. По сути, веб-работники работают независимо от основного потока. Любой код, выполняемый работником, должен храниться отдельно от остальной части вашего кода JavaScript в его собственном JS файле. Кроме того, если веб-работники нуждаются в конкретных данных для правильной работы, вам необходимо передать эти данные при их запуске.

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

В любом случае, я решил написать базовый шаблон, чтобы показать вам, как подойти к этому. Проверьте это ниже. Не стесняйтесь задавать вопросы/критиковать/и т.д.

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

function startWorker(dataObj)
{
    var message = {},
        worker;

      try
      {
        worker = new Worker('workers/getFileData.js');
      } 
      catch(error) 
      {
        // Throw error
      }

    message.data = dataObj;

    // all data is communicated to the worker in JSON format
    message = JSON.stringify(message);

    // This is the function that will handle all data returned by the worker
    worker.onMessage = function(e)
    {
        display(JSON.parse(e.data));
    }

    worker.postMessage(message);
}

Затем в отдельном файле, предназначенном для рабочего (как вы видите в приведенном выше коде, я назвал свой файл getFileData.js), напишите что-нибудь вроде следующего...

function fetchFiles(source)
{
    // Put your code here
    // Keep in mind that any requests made should be synchronous as this should not
    // impact the main thread
}

function loadFile(file)
{
    // Put your code here
    // Keep in mind that any requests made should be synchronous as this should not
    // impact the main thread
}

onmessage = function(e)
{
    var response = [],
        data = JSON.parse(e.data),
        file_list = fetchFiles(data.source),
        file, fileData;

    if (!file_list) 
    {
        response.push('failed to fetch list');
    }
    else 
    {
        for (file in file_list) 
        { // iteration, not enumeration
            fileData = loadFile(file);

            if (!fileData) 
            {
                response.push('failed to load: ' + file);
            } 
            else 
            {
                response.push(fileData);
            }
        }
    }

    response = JSON.stringify(response);

    postMessage(response);

    close();
}

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

Переполнение стека - веб-рабочие и синхронные запросы

Ответ 5

async - популярная асинхронная библиотека управления потоками, которая часто используется с node.js. Я никогда не использовал его в браузере, но, видимо, он работает и там.

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

Я предполагаю, что ваши две функции async принимают обратные вызовы. Если они этого не сделают, мне потребуется дополнительная информация о том, как они предназначены для использования (они запускают события после завершения? И т.д.).

async.waterfall([
  function (done) {
    fetchFiles(source, function(list) {
      if (!list) done('failed to fetch file list');
      else done(null, list);
    });
    // alternatively you could simply fetchFiles(source, done) here, and handle
    // the null result in the next function.
  },

  function (file_list, done) {
    var loadHandler = function (memo, file, cb) {
      loadFile(file, function(data) {
        if (!data) {
          display('failed to load: ' + file);
        } else {
          display(data);
        }
        // if any of the callbacks to `map` returned an error, it would halt 
        // execution and pass that error to the final callback.  So we don't pass
        // an error here, but rather a tuple of the file and load result.
        cb(null, [file, !!data]);
      });
    };
    async.map(file_list, loadHandler, done);
  }
], function(err, result) {
  if (err) return display(err);
  // All files loaded! (or failed to load)
  // result would be an array of tuples like [[file, bool file loaded?], ...]
});

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

Конечно, вы могли бы добавить любое количество различных асинхронных обратных вызовов между ними или вокруг них, без необходимости вообще изменять структуру кода. waterfall - фактически только 1 из 10 различных структур управления потоком, поэтому у вас есть много вариантов (хотя я почти всегда заканчиваю использование auto, что позволяет смешивать параллельное и последовательное выполнение в той же функции с помощью Makefile, например синтаксис требований).

Ответ 6

У меня была эта проблема с webapp, над которым я работаю, и вот как я ее решил (без библиотек).

Шаг 1: написал очень легкую реализацию pubsub. Ничего особенного. Подписаться, Отказаться от подписки, Опубликовать и войти в систему. Все (с комментариями) добавляет 93 строки Javascript. 2.7kb перед gzip.

Шаг 2. Отменил процесс, который вы пытались выполнить, позволив реализации pubsub сделать тяжелый подъем. Вот пример:

// listen for when files have been fetched and set up what to do when it comes in
pubsub.notification.subscribe(
    "processFetchedResults", // notification to subscribe to
    "fetchedFilesProcesser", // subscriber

    /* what to do when files have been fetched */ 
    function(params) {

        var file_list = params.notificationParams.file_list;

        for (file in file_list) { // iteration, not enumeration
        var data = loadFile(file);

        if (!data) {
            display('failed to load: ' + file);
        } else {
            display(data);
        }
    }
);    

// trigger fetch files 
function fetchFiles(source) {

   // ajax call to source
   // on response code 200 publish "processFetchedResults"
   // set publish parameters as ajax call response
   pubsub.notification.publish("processFetchedResults", ajaxResponse, "fetchFilesFunction");
}

Конечно, это очень многословие в настройке и малое по волшебству за кулисами. Вот некоторые технические детали:

  • Я использую setTimeout для обработки подписок. Таким образом, они работают неблокирующимся образом.

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

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

Ура!