Обработка взаимозависимых и/или многоуровневых асинхронных вызовов
В качестве примера предположим, что я хочу получить список файлов откуда-то, а затем загрузить содержимое этих файлов и, наконец, отобразить их пользователю. В синхронной модели это было бы что-то вроде этого (псевдокод):
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"
и сделать несколько действий после ответа (например, ведение журнала и обработка), сохраняя при этом их отдельные, крошечные и легко управляемые блоки кода.
-
Вышеприведенный пример кода не относится к резервным копиям или не запускает надлежащие проверки. Я уверен, что для получения стандартов производства потребуется немного инструментария. Просто хотел показать вам, насколько это возможно, и насколько независим от библиотеки ваше решение.
Ура!