[UPD] Chain.js: связываем синхронные и асинхронные функции в цепи

в 9:20, , рубрики: deferred, javascript, promises, Веб-разработка, метки: , , ,

Chain.js — маленькая библиотека, сделанная для создания цепочек из синхронных и асинхронных функций. Идея цепочек родилась после знакомства с Common JS Promises. Само определение «обещаний» говорит, что promise — это значение выполнения одной операции. Если вам захотелось что-то изобрести, придумать или создать, то вы просто обязаны попытаться связать эти операции в цепочки. Конечно, вы не обязаны, и это естественно, но для меня это стало основным мотивом. Перед этим я действительно столкнулся с некоторыми неудобствами связывания promise-операций, хотя ожидал что именно с этим они мне и помогут.

Начало

Само существование Common JS Promises мне было известно давно. К взаимному проникновению меня толкнул Angular.js, который имеет свою реализацию promise/deferred. Немного помучившись с реализацией тех самых цепочек на основе promises, стало понятно, что promises для этого и не предназначены. Поэтому было принято решение изобрести велосипед пойти дальше и сделать необходимую функциональность самому.

К делу

В ходе размышлений о программном интерфейсе, Сhain.js обзавелась списком из 5 функций: 1 конструктор и 4 метода. Сжатая реализация имеет размер в 2.03КБ, полная (без комментариев ^_^) реализация имеет размер в 3.15КБ. Скачать эти файлы можно на странице сделанной специально для Chain.js. Прямых ссылок нет, боюсь, могу сломать.
Установка тривиальна, не отличается от других скриптов.

<script src="js/chain.dev.js" type="text/javascript"></script>

После этого библиотека доступна в глобальной области видимости (window, видимо) под именем Chain. Chain является конструктором новой цепи и имеет два основных варианта вызова (дополнительные варианты можете придумать сами):

var testChain = Chain();
// или
var anotherChain = new Chain;

Теперь мы можем ощутить всю мощь создавать наши цепи, для этого у нас есть 3 метода:

  • .then(Function | Array.<Function>)
  • .defer(Function | Array.<Function>)
  • .when(Chain | Array.<Chain>)

Метод .then принимает синхронные функции, .defer — асинхронные, .when — объекты Chain. Все 3 метода возвращают объект Chain, для которого вызываются (прямо как jQuery chaining). Также эти методы могут принимать либо один требуемый аргумент, либо массив таких аргументов. Для методов .then и .defer результат выполнения предыдущего звена передается следующему в указанную функцию первым аргументом.
Выполняется цепь только тогда, когда будет вызван метод .end, он же последний метод библиотеки.
Имеет такую сигнатурку:

  • .end([Function])

Функция будет вызвана при завершении всех операций в цепи. Указание функции необязательно.
Примеры для понимания:

var calculate = Chain();

calculate.
    then(function() { // первая функция вызывается с аргументом undefined
      return 0;
    }).
    then(function(result) { // не трудно догадаться, result равен 0
      return result + 5;
    }).
    then(function(result) { // result равен 5
      return result + 10;
    }).
    end(function(result) { console.log(result); }); // выведет 15

Иная запись с тем же результатом:

var calculate = new Chain;

function zero() { return 0; }
function plus5(num) { return num + 5; }
function plus10(num) { return num + 10; }
function log(result) { console.log(result); }

calculate.
    then([zero, plus5, plus10]).
    end(log); // выведет 15

Проделаем тоже самое асинхронно:

var calculate = Chain();

calculate.
    defer(function(n, done) {
      // первый аргумент будет равен undefined,
      // второй - функция, которую надо вызвать при завершении нашей операции
      done(0);
    }).
    defer(function(result, done) { // result равен 0
      // та самая асинхронность
      setTimeout(function() {
          done(result + 5);
      }, 1000);
    }).
    defer(function(result, done) { // result равен 5
      done(result + 10);
    }).
    end(function(result) { console.log(result); }); // выведет 15

Тоже самое, по-другому:

var calculate = new Chain;

function zero(n, done) { done(0); }
function plus5(num, done) {
  setTimeout(function() {
    done(num + 5);
  }, 1000);
}
function plus10(num, done) { done(num + 10); }
function log(result) { console.log(result); }

calculate.
    defer([zero, plus5, plus10]).
    end(log); // выведет 15

Ну и наконец, метод .when служит для соединения остальных ваших цепей. После выполнения метода .when в следующее звено передается массив с результатом выполнения предыдущих звеньев-цепей. Пример:

var five = Chain(), ten = Chain();

five.defer(function(n, done) {
  setTimeout(function() {
    done(5);
  }, 1000);
});

ten.
    when(five).
    then(function(results) { return results[0] + 5; }); // results будет равен [5]

Chain().
    when([ten, five, ten]).
    end(function(results) { console.log(results); }); // выведет [10, 5, 10]

Особенности работы

Цепи переданные в метод .when выполняются параллельно. Последовательность в результирующем массиве будет соответствовать последовательности соединения(добавления) указанных цепей, см. последний пример.

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

Результат выполнения цепи сохраняется и будет возвращаться при каждом вызове метода .end, цепь запущена не будет. На самом деле, метод .end может принимать второй аргумент, заставляющий запустить цепь еще раз, но это поведение не доведено до ума. Если Chain.js придется вам по душе, то беру на себя обязательство с этим поведением разобраться.
Также, не реализовано прерывание выполнения цепи, хотя это возможно и легко осуществимо.

Конец

А конец ли? Приветствую ваши комментарии, предложения, пожелания. Спасибо за внимание.

Обновление. Сравнение с решениями на других библиотек

После третьего комментария с вопросом на тему, чем же Chain.js отличается от других библиотек, мне стало неудобно молчать. Я связался с KeepYourMind и попросил его помочь с реализацией простой задачи на when.js. KeepYourMind согласился помочь и очень скоро показал примерное решение предложенной задачи. Для примера была предложена такая задача: асинхронно получаем домен интересующего нас сервиса, затем генерируем ссылку на какой-либо конкретный адрес, получаем по этому адресу данные, и показываем данные пользователю. Грубо изображу это таким представлением:

  • получить доменное имя -> сгенерировать ссылку -> получить данные -> отобразить пользователю

После этого я адаптировал пример решения KeepYourMind под Chain.js. Ссылки с кодом на Github Gist:

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

Также, в ходе обсуждения кода с KeepYourMind, родилась идея расширить Chain.js, чтобы стало возможным использование такого варианта решения. Еще раз спасибо за внимание.

Автор: PhoSor

Источник



https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js