Как стать автором
Обновить

Комментарии 33

Спасибо, прочел с интересом.
Зашел, чтобы написать этот комментарий.
В JQuery еще есть хорошая реализация паттерна Deferred.
Да и в dojo она неплохая в принципе.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
waterfall — почти то же самое что и сабж. Спасибо.
НЛО прилетело и опубликовало эту надпись здесь
А почему вложенные коллбеки это не есть гуд?
CallBack — в реальности это не возврат управления, как можно подумать из названия, а вложенный вызов одной из функции «родителя», «углубляющий» реальную вложенность функций.
Т.е. если рассмотреть стек, то каждый вложенный callback будет добавлять данные в него, а вовсе не возвращать вершину стека обратно (как хотелось бы).

Поэтому действительно верным является только путь посылки сообщений в менеджер очереди.
Цикл обработки сообщений (обычно) отрабатывается после выхода из всех вложенных функций — т.е. на базовом уровне.
Колбэк можно вызывать по таймеру с минимальной задержкой, если жалко стэк.
НЛО прилетело и опубликовало эту надпись здесь
Мы же говорим об асинхронных процессах типа аякса и анимации, о которых мы никогда не знаем, когда они закончатся?
НЛО прилетело и опубликовало эту надпись здесь
Во-первых, не всегда, а только если колбэк вызывается из функции явно (в конце функции, например). А такая ситуация не требует вложенных колбеков в принципе, можно просто вернуть результат в вышестоящую функцию и продолжить выполнение там. В других же случаях, когда используется XHR, или асинхронное чтение из файла, или еще что-то подобное — вызов приходит из среды исполнения.

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

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

Например, у меня обрабатываются до 10 запросов пакетом. Очень часто ответы приходят «пакетом», парсер режет их и «возвращает отправителям».
В примере из топика я наоборот нигде не вижу прямых вызовов runNext в теле функции.

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

Тот же пример с parse, addToDataBase и т.п. совершенно не ориентирован на то, что у нас может повалиться парсинг, либо добавление данных в БД.

В том же JSDeffered Можно прервать цепь событий, и нормально обработать ошибку. А в текущем подходе этого не наблюдается.

З.Ы. В свое время что-то похожее писал для AS, и там была та же проблема.
Возможно, вы правы, и стоит добавить обработку исключений.
Попробуйте больше использовать один из принципов ООП — инкапсуляцию, и вы увидите как жизнь станет лучше.
Или другими словами — абстрагируйтесь от конкретных задач и Ваш костыль Вам больше не понадобится.

Сам применяю этот метод в NodeJS и в итоге не имею спагетти-код.
Вы имеете ввиду расширение прототипа Function? Не могли бы вы показать небольшой пример?
Нет.
Я могу написать конкретный пример, но для этого нужна конкретная задача. Ваша задача с парсером сайтов не достаточно конкретна, необходимо описание от начала до конца.
И если я начну писать в контексте вашей задачи, получится слишком много буков, может и на топик потянуть.
Но основная мысль в том, что бы скрыть явную логическую цепь за абстрактными сущностями. Саму же цепь можно реализовать, например, через конечный автомат или любой другой паттерн. Опять же, всё зависит от конкретной задачи.
Не я автор топика :) По поводу задачи — несколько запросов к БД, в зависимости от обстоятельств. Хм… К примеру у нас есть РСУБД, а в ней таблица person, в которой есть поле person_type. Наша задача написать функцию, которая по переданному ей параметру person_id прочтёт запись из таблицы person, а на основе этой записи прочтёт доп.сведения из таблицы person_#{person_type}. Полученный результат сохранит в файл. Функция должна вернуть массив с полученными данными и временем, когда файл был дописан. Итого — 3 неблокирующие операции. php и js примеры (написал на коленке).

По поводу статьи — неплохая мысль. Если предложенный вами подход удобен, есть смысл задуматься об отказе от Fibers.
Как бы я реализовал твою задачу.

1. Если следовать принципам ООП, функционал работы с данными пользователя нужно разделить на 3 отдельных метода этого самого пользователя: один метод для сохранения, 2 для получения данных (т.к. типы запросов разные, соответственно и данные разные).
2. После того как функционал разделён, нужно реализовать интерфейс обмена данными. Это может быть явная передача callback-а в вызываемый метод, либо реализация событийной модели на основе наследования от require('events').EventEmitter:
function Person(){
  this.id = id;
};
Person.prototype = new events.EventEmitter;
Person.prototype.saveData = function(data, callback){
       this.saveFile( 'person_' + this.id, callback);
};
Person.prototype.getData = function(type, callback){};


Далее, нам как-то нужно реализовать последовательность вызовов методов нашего пользователя. Это будет происходить уже вне реализации Person. Для этого не обязательно явно создавать цепочку из вызовов функции wait с передачей кучи callback-ов. Вместо этого можно реализовать абстрактный конструктор, скажем, Actions, который будет менять состояние некоего объекта по заданному сценарию. Но сначала реализуем метод setState, который будет реагировать на смену состояния объекта:
Person.prototype.setState = function(state, data, callback) {
  switch (state) {
    case 'saveDataInFile':
      this.saveData(data, callback);
    break;
  }
  this._state = state;
};

На самом деле можно было бы реализовать это в виде обработчика события setState, в конструкторе Person:
this.on('setState', onSetState);

Тут уж кому как больше нравится. Код функции onSetState не должен находиться в конструкторе Person.

Конструктор Actions:
function Actions(instance) {
  var self = this;

  this.nextState  = function (error, data) {
    if (error) {
      instance.emit('error', error);
      return;
    }

    self.currentState = self.actions.shift();

    if (self.currentState) {
    /* Здесь может быть генерация события или вызов callback-а.
    К примеру, каждое действие может быть описано объектом,
    включающим в себя имя, данные и callback, если это необходимо. */
      instance.setState(self.currentState, data, self.nextState);
    /*
      Либо так, если реализуется через событие setState:
      instance.emit('setState', data, self.nextState);
      */
    } else { // Закончили цикл действий.
      delete self.currentState;
      self.callback && self.callback(data, instance);
      delete self.callback;
    }
  };
}

Actions.prototype.run = function () {
  arguments = [].prototype.slice.call(arguments);
  if (typeof arguments[arguments.length - 1] == 'function') {
    this.callback = arguments.pop();
  }
  this.actions = arguments;
  process.nextTick(this.nextState);
};


Таким образом, выполнение конкретных действий сводится к:
var user = new Person('id');
user.on('error', function(error) {}); // Обработка ошибок на любом этапе выполнения.

var userActions = new Actions(user);
userActions.run('getDataFromDB', 'getDataFromDB2', 'saveDataInFile', function(data, user) {});

Какие преимущества я тут вижу:
1. Разделение функционала => гибкость.
2. Появился инструмент последовательного изменения состояний объектов. На самом деле применений у него может быть очень много.
3. При запуске последовательности каких-либо действий не создаются дополнительных функций и замыканий, как в подходе автора блога и не только.

PS: Я никого не хочу учить. Я только описал свои мысли в слух по поводу решения конкретной задачи. Выбор всегда остается за вами. Извиняюсь за большой объем.
PSS: Код на работоспособность не проверял. Так же хочу заметить, что это не конечный результат, а лишь грубое описание смысла решения.
Спасибо. Интересное решение, основанное на очереди задач. На мой взгляд имеет две проблемы:

1. не прозрачное
2. тонны кода

Т.е. избавившись от кучи callback-ов мы получили «ООП головного мозга» подвида «очередь». Мне кажется, что при большом количестве задач, вроде той, что я описал, система может усложниться в несколько раз, и работать с ней будет чудовищно сложно. Дабы избежать этого — систему можно наворачивать (к примеру: продумать возможность удобной передачи аргументов между стадиями), что, в свою очередь, ещё более усложнит схему, но сделает её более гибкой.

Но это всё только на первый взгляд. Не могу судить адекватно пока не попробую её в деле. В любом случае, спасибо, возьму на заметку.
Если не против, я прокомментирую проблемы:
1. Возможно в таком виде да, но ведь никто не запрещает переписать, используя знакомые конструкции и термины. Человек, ведь он такой, ко всему может привыкнуть :)
2. Я забыл добавить, что по-хорошему Person и Actions (кстати, не совсем удачное имя) нужно вынести в отдельные модули. Эти «тонны» скрыты (инкапсуляция), вызов сводится к 1 строке (если не брать в счет создание объекта пользователя). Так же с ростом приложения будут появляться новые методы и/или состояния у объектов пользователей, но мы ведь знаем, где им место? :)

На самом деле есть много неучтенных кейсов, но это дело техники. Например, запуск нескольких независимых очередей. Если есть интерес, могу допилить до юзабельного состояния и выкатить на github.
Что делать в случае когда логика не столь прямолинейна. Когда для понимания какая нужна следующая стадия нужно знать результаты стадии предыдущей. Особенно если учесть, что класс Person не может знать о всех возможных последовательностях применения его методов извне?
Это легко решаемо. Я писал в комментарии к коду:
К примеру, каждое действие может быть описано объектом, включающим в себя имя, данные и callback, если это необходимо.

Пример обработчика:
function onNextState (intance, actions) {
  actions // Экземпляр конструктора Actions
  instance // Объект, обрабатываемый объектом actions
  return {}; // Данные, которые будут переданы в instance
}


Выглядеть это будет примерно так:
userActions.run({
  name: 'getDataFromDB',
  callback: onNextState
}, function(data, user) {});


Или так, что бы не создавать лишних объектов:
userActions.on('nextState', onNextState);


Выбрать по вкусу. Взгляните на функцию onNextState. Имея такие аргументы, можно не плодить обработчики, так как все нужные данные уже есть и замыканий не требуется.
Так же обратите внимание на доступность свойств actions.currentAction и actions.actions (да, стоит переименовать). Порядок (массив actions) можно изменить в любое время. Кстати, а в реализации автора поста такое возможно?

Конечно для этого необходимо изменить реализацию Actions, но это 2 строчки.
Понятно. Получается что:

1. прямолинейная логика — список строк-стадий.
2. сложная логика — либо спагетти, либо куча функций.

В таком случае, на мой взгляд, на серверной стороне проще использовать Sync (ну или какую-нибудь другую оболочку над fibers). Ведь это удобне и читабельнее. Ну а на клиентской, конечно, такие «финты» не прокатят :)
Да пожалуйста, я не против :)

Добавлю только, что если в функции onNextState требуется асинхронный вызов, легко можно реализовать метод Actions.prototype.stop, а в качестве callback-а в асинхронный вызов передать actions.nextState (для которого контекст не важен), который запустит дальнейшее выполнение.
Велосипедостроение процветает) На хабре был где-то коммент с коллекцией библиотек (порядка 20-30), которые делают то же самое.
Тут не библиотека, а очень компактная функция :)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации