Асинхронные API и объект Deferred в деталях

Материал из support.qbpro.ru

оригинал статьи анализ кода

Большинство современных языков программирования позволяют использовать асинхронно выполняемые блоки кода. Вместе с гибкостью, получаемой при использовании асинхронного подхода, рискнувший его применить также получает более сложный для понимания и поддержки код. Однако, любое усложнение, с которым сталкиваются программисты, как правило, находит практическое решение в виде нового подхода или повышения уровня абстракции. В случае асинхронного программирования таким средством служит объект типа отложенный результат или deferred (англ. deferred — отложенный, отсроченный).

В статье будет рассказано о базовых подходах к возврату асинхронных результатов, функциях обратного вызова, объектах deferred и их возможностях. Будут приведены примеры на языке JavaScript, а также произведён разбор типового объекта deferred. Статья будет полезна программистам, начинающим постигать асинхронное программирование, а также знакомым с ним, но не владеющим объектом deferred.

Синхронный и асинхронный вызовы

Любую функцию можно описать в синхронном и асинхронном виде. Предположим, что у нас есть функция calc, выполняющая некоторое вычисление.

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

var result = calc();
another_calc(result * 2);

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

В случае асинхронного вызова, мы не можем получить результат на месте. Вызывая функцию calc, мы лишь укажем на необходимость выполнить вычисление и получить его результат. При этом следующая строка начнёт выполняться не дожидаясь выполнения предыдущей. Тем не менее, получить результат нам как-то надо, и тут на помощь приходит коллбек (callback) — функция, которая будет вызвана системой по приходу результата вычисления. Результат будет передан в эту функцию как аргумент.

calc(function (result) {
   another_calc(result * 2);
});
no_cares_about_result();

Из примера видно, что функция теперь имеет сигнатуру: calc(callback), а callback принимает в качестве первого параметра результат.

Так как calc выполняется асинхронно, функция no_cares_about_result не сможет обратиться к её результату, и, вообще говоря, может быть выполнена раньше чем коллбек (если говорить конкретно о JavaScript, то гарантируется, что она всегда будет выполнена раньше; об этом будет рассказано чуть ниже).

Согласитесь, такой код уже стал несколько сложнее для восприятия, при той же смысловой нагрузке, что и его «прямолиненый» синхронный аналог. В чём же выгода от использования асинхронного подхода? Прежде всего — в разумном использовании ресурсов системы. Например, если calc является трудоёмким вычислением, которое может затратить много времени, или использует какой-то внешний ресурс, на использование которого накладывается определённая задержка, то при синхронном подходе весь последующий код будет вынужден ожидать результата и не будет выполняться, пока не выполнится calc. Используя асинхронный подход можно явно указать какой участок кода зависит от некоторого результата, а какой к результату индифферентен. В примере, no_cares_about_result явно не использует результат, и, следовательно, ему не требуется его ожидать. Участок кода внутри коллбека же будет выполнен только после получения результата.

Вообще говоря, большинство API, по своей природе, являются асинхронными, но могут мимикрировать под синхронные: доступ к удалённым ресурсам, запросы к БД, даже файловое API ­— асинхронные. Если API «притворяется» синхронным, то успех такого «притворства» связан с задержками результата: чем меньше задержка, тем лучше. То же файловое API, работая с локальной машиной, показывает небольшие задержки и зачастую реализуется как синхронное. Работа с удалёнными ресурсами и доступ к БД всё чаще реализуется асинхронно.

Многоуровневые вызовы

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

var result = calc_one();
result = calc_two(result * 2);
result = calc_three(result + 42);
// using result

Код приобретёт следующий вид:

calc_one(function (result) {
   calc_two(result * 2, function (result) {
      calc_three(result + 42, function (result) {
         // using result
      });
   });
});

Во-первых, данный код стал «многоуровневым», хотя, по выполняемым действиям он аналогичен синхронному. Во-вторых, в сигнатурах функций calc_two, calc_three смешаны входные параметры и коллбек, который, по сути является местом возврата результата, то есть выходным параметром. В-третьих, каждая функция может завершиться с ошибкой, и результат не будет получен.

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

Асинхронный результат

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

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

Важной отличительной особенностью такого объекта также является специфика его состояний. Такой объект может быть в двух состояниях: 1) нет результата и 2) есть результат. Причём переход возможен только из первого состояния во второе. Когда результат будет получен, то уже невозможно перейти в состояние его отсутствия или в состояние с другим результатом.

Рассмотрим следующий простой интерфейс для данного объекта:

function Deferred () // constructor
function on (callback)
function resolve (result)

Метод on принимает коллбек. Коллбек будет вызван как только будет доступен результат и он будет передан в качестве параметра. Здесь полная аналогия с обычным коллбеком, передаваемым в качестве параметра. На момент регистрации коллбека объект может находиться в состоянии с результатом и без. В случае, если результата ещё нет, коллбек будет вызван по его приходу. В случае, если результат уже есть, коллбек будет вызван немедленно. В обоих случаях коллбек вызывается однократно и получает результат.

Метод resolve позволяет перевести (разрезолвить) объект в состояние с результатом и указать этот результат. Этот метод является идемпотентным, то есть повторные вызовы resolve не будут изменять объект. При переходе в состояние с результатом будут вызваны все зарегистрированные коллбеки, а все коллбеки, которые будут зарегистрованы после вызова resolve станут вызываться мгновенно. В обоих случаях (регистрация до и после вызова resolve) коллбэки будут получать результат, в силу того, что объект хранит его.

Объект с таким поведением называется deferred (а также известен под именами promise и future). Перед простыми коллбеками он имеет ряд преимуществ:

1. Абстрагирование асинхронной функции от результата: теперь каждой асинхронной функции не требуется предоставлять параметры-коллбеки. Подписка на результат остаётся за клиентом кода. Например, можно вообще не подписываться на результат, если он нам не нужен (аналогичен передачи noop-функции в качестве коллбека). Интерфейс асинхронной функции становится чище: он имеет только значимые входные параметры.

2. Абстрагирование от состояния результата: клиенту кода не нужно проверять текущее состояние результата, он просто подписывает обработчик и не задумывается, пришёл результат или ещё нет.

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

4. Ряд дополнительных удобств, в числе которых, например, «алгебра» объектов deferred, которая позволяет определять отношения между ними, запускать их в цепочке или после успешного завершения группы таких объектов.

Рассмотрим следующий пример. Пусть имеется асинхронная функция getData(id, onSuccess), которая принимает два параметра: id некоторого элемента, который мы хотим получить и коллбек для получения результата. Типичный код её использования будет выглядеть так:

getData(id, function (item) {
   // do some actions with item
});

Перепишем это с использованием Deferred. Функция теперь имеет сигнатуру getData(id) и используется следующим образом:

getData(id).on(function (item) {
   // do some actions with item
});

В данном случае код практически не усложнился, скорее просто изменился подход. Результат теперь передаётся через возвращаемое значение функции в качестве deferred. Однако, как станет заметно далее, в более сложных случаях использование deferred даёт некоторое преимущество в читаемости кода.

Обработка ошибок

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

Такой результат, также как и успешный, реализуется в виде коллбека (который иногда называется errback, игра слов от error и back).

Давайте усилим наш учебный объект Deferred так, чтобы он мог предоставлять подписку отдельно на успех и на неудачу, а именно переработаем методы on и resolve.

function on (state, callback)

В качестве первого параметра можно передавать значение перечислимого типа с двумя значениями, например E_SUCCESS, E_ERROR. Для читаемости, будем использовать в примерах простые строковые значения: 'success', 'error'. Также, усилим данный метод, обязав его возвращать сам объект Deferred. Это позволит использовать цепочки подписок (приём весьма характерный конкретно для JavaScript).

Соответственно изменяется и метод resolve:

function resolve (state, result)

В качестве первого параметра передаётся состояние, в которое должен перейти объект Deferred (error, success), а в качестве второго — результат. На такой модифицированный объект по-прежнему распространяется правило состояний: после перехода в состояние с результатом, объект не может изменить своё состояние на иное. Это означает, что если объект перешёл, например, в состояние success, то все обработчики, зарегистрированные на ошибку не сработают никогда, и наоборот.

Итак, пусть наша функция getData может завершиться с некоторой ошибкой (нет данных, неправильные входные данные, сбой и т.п.). Код примет следующий вид:

getData(id)
.on('success', function (item) {
   // do some actions with item
})
.on('error', function (err_code) {
   // deal with error
});

Рассмотрим более реалистичный пример, а именно, возьмём типовой метод fs.readFile из стандартной библиотеки Node.js. Этот метод служит для чтения файла. В начале статьи упоминалось, что практически любую функцию можно написать либо в синхронном, либо в асинхронном стиле. В стандартной библиотеке Node.js файловое API определено в обоих стилях, у каждой функции есть свой синхронный аналог.

Для примера мы используем асинхронный вариант readFile и адаптируем его под использование Deferred.

function readFileDeferred (filename, options)
{
   var result = new Deferred;
   fs.readFile(filename, options, function (err, data)
   {
      if (err)
      {
         result.resolve('error', err);
      }
      else
      {
         result.resolve('success', data);
      }
   });
   return result;
}

Такая функция несколько удобней в использовании, потому как позволяет регистрировать функции на успех и на ошибку отдельно.

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

Расширенные возможности объектов Deferred

  1. Неограниченное количество вариантов результата. В примере был использован объект Deferred с двумя возможными результатами: success и error. Ничто не мешает использовать любые другие (кастомные) варианты. Благо, мы использовали строковое значение в качестве state, это позволяет определять любой набор результатов, не изменяя никакой перечислимый тип.
  2. Возможность подписки на все варианты результата. Это может быть использовано для разного рода обобщённых обработчиков (наибольший смысл это имеет вкупе с пунктом 1.).
  3. Создание суб-объекта promise. Из интерфейса объекта Deferred видно, что клиентский код имеет доступ к методу resolve, хотя, по сути, ему требуется только возможность подписки. Суть данного улучшения состоит во введении метода promise, который возвращает «подмножество» объекта Deferred, из которого доступна только подписка, но не установка результата.
  4. Передача состояния от одного deferred к другому, опционально, подвергая результат преобразованию. Это бывает очень полезным при многоуровневых вызовах.
  5. Создание deferred, который зависит от результата набора других deferred. Суть данного улучшения в том, чтобы подписываться на результат группы асинхронных операций.

Пусть нам нужно зачитать два файла и сделать с обоими что-нибудь интересное. Используем нашу функцию readFileDeferred для этого:

var r1 = readFileDeferred('./session.data'),
    r2 = readFileDeferred('./data/user.data');

var r3 = Deferred.all(r1, r2);

r3.on('success', function (session, user) {
   session = JSON.parse(session);
   user = JSON.parse(user);
   console.log('All data recieved', session, user);
}).on('error', function (err_code) {
   console.error('Error occured', err_code);
});

Deferred.all создаёт новый объект Deferred, который перейдёт в состояние success, если все переданные аргументы перейдут в это состояние. При этом он также получит результаты всех deferred в качестве аргументов. Если хотя бы один аргумент перейдёт в состояние error, то и результат Deferred.all также перейдёт в это состояние, и получит в качестве результата результат аргумента, перешедшего в состояние error.

Особенности deferred в JavaScript

Стоит отметить тот момент, что в JavaScript отсутствует многопоточность. Если коллбек был установлен по setInterval / setTimeout или по событиям, он не может прервать выполнение текущего кода, или выполняться параллельно с ним. Это означает, что даже если результат асинхронной функции придёт мгновенно, он всё равно будет получен лишь после завершения выполнения текущего кода.

В JavaScript функции могут вызываться с любым числом параметров, а также с любым контекстом. Это позволяет передавать в коллбеки столько параметров, сколько потребуется. Например, если асинхронная функция возвращает пару значений (X, Y), то их можно передать в виде объекта с двумя полями, или списка с двумя значениями (импровизированный аналог кортежа), а можно использовать два первых аргумента коллбека для этой цели.

Вызов коллбека в таком случае может принимать следующий вид: callback.call(this, X, Y);

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

Существующие deferred

  1. В jQuery существует объект $.Deferred (документация). Поддерживает подписку на success, error, также поддерживаются progress-нотификации: промежуточные события, генерируемые до прихода результата; можно передавать состояние другому Deferred (метод then), можно регистрировать Deferred по результату списка Deferred ($.when), можно создавать promise. Все ajax-методы библиотеки возвращают promise такого объекта.
  2. Dojo Toolkit содержит объект Deferred (документация).
  3. В братском языке Python, в event-driven фреймворке Twisted есть объект Deferred со схожими возможностями (документация). Поддерживает подписку на success, error и на оба результата. Можно ставить объект на паузу.
  4. Охваченный интересом к Deferred, я написал свой вариант данного объекта (документация, исходный код, тесты). Поддерживается ряд возможностей, описанных в данной статье.

Документация с GitHub

оригинал документации

deferred

The idea in using deferred is to distinct state of asynchronous query from it's result's handlers.

Deferred object useful for running code only when some condition is accomplished. It saves state of condition, so code will be executed exactly when it's needed.

Deferred object can be used in places where we expect some result, but it is asynchronous. The deferred object returned instead, and handlers are used for upcoming result.

// returns deferred object
function read (filename)
{
    var result = deferred();
    fs.readFile(filename, function (err, data)
    {
        if (err) result.resolve(false);
        else result.resolve(true, data);
    });
    return result;
}

var r = read('./test.txt');
r.success(function (data) { console.log(data); });
r.fail(function () { console.log('error during data reading'); });

Concept

Deferred object considers non-limited number of possible result states, each named by string key. When deferred is resolved to some state it cannot be resolved to another. All handlers registered to that state will be invoked, all other handlers will not. You're free to register handlers as before resolving of deferred as after. In both cases the proper handlers will be invoked.

API

Объект Deferred может быть инициирован двумя способами: через new или как функция

var d1 = new deferred, d2 = deferred();

.on(variant, handler)

.on(variant, handler)

Регистрирует обработчик который вызовется немедленно если объект Deferred разрешен (или как только будет разрешен) в вариант состояния. Registers handler that will be invoked if deferred is resolved (or will be resolved) to variant state.

Пример:

.on('success', function () { });
.on('fail', function () { });
.on('some_another_variant', function () { });

Так же короткая форма:

.on(true, handler);  // means `success`
.on(false, handler); // means `fail`

Существуют так же специальные методы:

.success(handler)

Регистрирует обработчик для варианта состояния success.

.fail(handler)

Регистрирует обработчик для варианта состояния fail.

.resolve(variant, arg1, arg2, arg3,...)

.resolve(variant, arg1, arg2, arg3,...)

Разрешает (переводит)обыект deferred в вариант состояния. Если для этого варианта уже были зарегистрированы обработчики, они будут исполнены. Любой обработчик, который будет зарегистрирован в дальнейшем для этого варианта состояния будет вызван немедленно.

Пример:

.resolve('success', 1, 2, 3);
.resolve('fail', 'some error');
.resolve('other_variant');

Короткая форма записи:

.resolve(true, 1, 2, 3);  // resolve with success
.resolve(false, 'some error'); // resolve with fail

Если были переданы дополнительные аргументы, они будут переданы обработчику (даже если обработчик был добавлен после разрешения)

.isResolved

Свойство, показывающее что объект deferred уже разрешен.

.resolution

Свойство, показывающее что в какое состояние разрешен объект deferred.

.ever(handler)

.ever(handler)

Регистрирует обработчик для любого возможного состояния разрешения.(success, fail and any custom variant).

Обработчики могут использовать(быть связаны с deffered) this объекта deffered, поэтому они могут использовать свойство resolution .

.passthru(deferred[, fn_arg])

.passthru(deferred[, fn_arg])

Присоединяет объект deferred, указанный в параметрах к текущему deferred таким образом, при при разрешении текущего deferred, объект deferred, указанный в параметре так же буден разрешен в то же состояние.

Также возможно присоединить deferred только для определенного варианта разрешения:

.passthru(variant, deferred[, fn_arg])

/*Connects deferred object in parameter to current deferred in variant state, so if current deferred is resolved to variant state, deferred will be resolved in this variant too.*/

В обоих случаях, если аргумент fn_arg не был передан, передаются все параметры. Иначе, должна быть передана функция fn_arg. Она будет использоваться для преобразования аргументов, которые будут переданы в разрешение deffered. Функция должна возвращать массив аргументов, так легче применить к deferred resolving. Так что, если нужно вернуть Единственный аргумент, который является массивом, его необходимо заключить в другой массив.

/*In both cases if no fn_arg supplied, passes through all parameters. Otherwise, function must be provided. It will be used to transform arguments that will be passed through to deferred resolution. The function must return array of arguments, so they can be easily applied to deferred resolving. So if you need to return only argument that is array, you must to enclose it in another array:*/

var d1 = new deferred, d2 = new deferred;
d1.resolve(true, [ 0, 1 ]); // first argument is array
d1.passthru(true, d2, function (a1, a2)
{
    // a1 - array
    // a2 - undefined
    var r = a1.slice();
    r[0] = r[0] * 2;
    r[1] = r[1] * 2;
    return [ r ]; // [[0, 2]];
});

It is also possible to return non-array value from transfrom function, it will be interpreted as first argument, but (as it was said above) take care about array as only argument.

.log(name) Log (console object) if deferred is resolved to any state. Logging outputs name, resolution and all arguments passed in resolving.

deferred.all(deferred1, deferred2, deferred3, ...) Constructor. Creates new deferred object that will be resolved in success when all passed deferreds resolved in success state. If any of passed deferreds resolved in fail state, deferred object will be resolved in fail.

If say in common, deferred object will be resolved in state of first deferred which resolution is not success, or in success in case of each deferred resolved such.

deferred.all can accept deferreds and arrays of deferreds, so all the following examples are equivalent:

deferred.all(d1, d2, d3);
deferred.all([d1, d2, d3]);
deferred.all(d1, [d2, d3]);
deferred.all(d1, [d2], [d3]);

If deferred resolved successfully it takes arguments where each argument is array of arguments of passed deferrded object in same order.

Consider we have deferreds d1 and 2d resolved in success.

var d1 = new deferred, d2 = new deferred;

var A = deferred.all(d1, d2);
d2.resolve(true, 'a', 'b', 'c')
d1.resolve(true, 1, 2, 3);

A.on(true, function (a1, a2)
{
    // a1 - array of arguments with which d1 resolved
    // a1 is [1, 2, 3]
    // a2 - array of arguments with which d2 resolved
    // a2 is ['a', 'b', 'c']
});

Note that arguments is passed in order of mentioning deferreds in constructor, not in resolution order (which is most common indefinite, due to asynchronous).

If deferred resolved not successfully, it takes arguments like first passed deferred that resolved not successfully.

var d1 = new deferred, d2 = new deferred;
d1.resolve(true, 1, 2, 3);
d2.resolve(false, 'x', 'y', 'z')

var A = deferred.all(d1, d2);

A.on(false, function (a1, a2, a3)
{
    // arguments is same as for d2 resolution
    // arguments is Arguments object ['x', 'y', 'z']
    // a1 == 'x'
    // a2 == 'y'
    // a3 == 'z'
});
deferred.bunch(variant, deferreds)

Constructor. Generalization of deferred.all. Accepts target variant and array of deferreds.

Creates new deferred that will be resolved to variant if all of deferreds is resolved to this variant. If any of deferreds is resolved else, deferred will be resolved to first variant that differs from target variant.

deferred.all defined as bunch:

.bunch('success', deferreds); Arguments are obey to same mechanics as in deferred.all.

==

Разбор кода

/* Статья
 * http://habrahabr.ru/post/175947/
 * Документация к модулю
 * https://github.com/StreetStrider/utils/blob/master/doc/deferred/deferred.md
 * Код
 * https://github.com/StreetStrider/utils/blob/master/src/deferred/deferred.js

 * Модуль deffered - это объект, содержащий информацию о том, что резуль-
 * тат когда-нибудь придёт или уже пришёл. Подписка на результат осуще-
 * ствляется через всё тот же коллбек, однако она инкапсулирована в этом
 * объекте и не обязывает асинхронные функции реализовывать коллбеки как
 * входные параметры.
 * По сути от объекта-результата требуется три вещи: реализовывать воз-
 * можность подписки на результат, возможность указывать приход резуль-
 * тата (это будет использоваться самой асинхронной функцией, а не кли-
 * ентом API) и хранение этого результата.
 * Важной отличительной особенностью такого объекта также является спе-
 * цифика его состояний. Такой объект может быть в двух состояниях: 
 * 1) нет результата 
 * 2) есть результат. 
 * Причём переход возможен только из первого состояния во второе. Когда 
 * результат будет получен, то уже невозможно перейти в состояние его 
 * отсутствия или в состояние с другим результатом.

 *          ==API==

 *          .on(variant, handler)
 * 
 * Регистрирует обработчик, который будет вызываться для данного 
 * варианта
 *  .on('success', function () { });
 *  .on('fail', function () { });
 *  .on('some_another_variant', function () { });
 * 
 * Краткая форма:
 *  .on(true, handler);  //вариант `success`(успех)
 *  .on(false, handler); // means `fail`(провал)
 * 
 * Специальная форма для true и false
 *  .success(handler) //обработчик для success.
 *  .fail(handler) //обработчик для fail.

 *          .ever = function (handler)
 * Регистрирует обработчики, выполняемые после обработчиков из массива 
 * this._variants (success, fail или любой пользовательский вариант).
 * Выполняются в независимости от значения variants, т.е. всегда.
 * Обработчики могут быть связаны с объектом Deferred через this, поэто-
 * му они могут использовать свойство resolution.
 * Через этот интерфейс можно регистрировать обработчики уже после пере-
 * вода объекта deffered в состояние 'есть результат'. Обработчики будут
 * выполнены немедленно.

 *          .resolve = function(variant, arg1, arg2, arg3,...)
 * 
 * Переводит объект deferred в вариант состояния есть результат в соот-
 * ветствии с вариантом в переданной переменной variant.
 * В случае нескольких регистрируемых обработчиков для данного состояния
 * они будут вызваны. Любой обработчик регистрируемый в дальшейшем для 
 * данного состояния будет вызван немедленно.

 *          .passthru = function (deferred, fn_arg)
 *          .passthru = function (variant, deferred, fn_arg)
 * 
 * Присоединяет объект deferred, указанный в параметрах к текущему 
 * deferred таким образом, при при разрешении текущего deferred, объект 
 * deferred, указанный в параметре так же буден разрешен в то же состоя-
 * ние. Также возможно присоединить deferred только для определенного 
 * варианта разрешения: .passthru(variant, deferred[, fn_arg])
 * В обоих случаях, если аргумент fn_arg не был передан, передаются все 
 * параметры. Иначе, должна быть передана функция fn_arg. Она будет 
 * использоваться для преобразования аргументы, которые будут переданы 
 * в resolve deffered. Функция должна возвращать массив аргументов, 
 * так легче применить к deferred resolving. Так что, если нужно вернуть
 * Единственный аргумент, который не является массивом, его необходимо 
 * за ключить в другой массив. Пример:
 *           var d1 = new deferred, d2 = new deferred;
 *           d1.resolve(true, [ 0, 1 ]); // first argument is array
 *           d1.passthru(true, d2, function (a1, a2)
 *                                {
 *                                  // a1 - array
 *                                  // a2 - undefined
 *                                  var r = a1.slice();
 *                                  r[0] = r[0] * 2;
 *                                  r[1] = r[1] * 2;
 *                                  return [ r ]; // [[0, 2]];
 *                                });
 * It is also possible to return non-array value from transfrom function,
 * it will be interpreted as first argument, but (as it was said above) 
 * take care about array as only argument. 

 *           .log = function (name)
 * 
 *  Log (console object) if deferred is resolved to any state. Logging 
 * outputs name, resolution and all arguments passed in resolving.

 *          .all = function all (deferred1, deferred2, deferred3, ...)
 * 
 * Конструктор. Создает новый объект deferred который будет переведен в 
 * состояние success только если все переданные объекты (deferred1, 
 * deferred2, deferred3, ...) перейдут в состояние success. Если любой 
 * из них перейдет в состояние fail, объект deferred так же перейдет в 
 * состояние fail.
 * В общем случае, объект deferred будет 
 * переведен в состояние первого из deferred переданных в аргементах, 
 * чье сотояние не будет success.
 * deferred.all может принимать как deferred отдельно, так и в массиве.
 * Записи в примере ниже эквивалентны:
 *           deferred.all(d1, d2, d3);
 *           deferred.all([d1, d2, d3]);
 *           deferred.all(d1, [d2, d3]);
 *           deferred.all(d1, [d2], [d3]);
 * Если объект deferred переведен в состояние success, он принимает 
 * аргументы, где каждый аргументы - массив аргументов, переданный 
 * объектами deferrded в том же порядке.
 * Для примера возьмем два deferreds d1 and d2 переведенных в success.
 *           var d1 = new deferred, d2 = new deferred;
 *           var A = deferred.all(d1, d2);
 *           d2.resolve(true, 'a', 'b', 'c');
 *           d1.resolve(true, 1, 2, 3);
 *           A.on(true, function (a1, a2)
 *           {
 *           // a1 - массив аргументов с которыми d1 был resolved
 *           // a1 is [1, 2, 3]
 *           // a2 - массив аргументов с которыми d2 был resolved
 *           // a2 is ['a', 'b', 'c']
 *           });
 * Note that arguments is passed in order of mentioning deferreds in 
 * constructor, not in resolution order (which is most common indefinite,
 * due to asynchronous).
 * Если deferred был resolved не success, он принимает аргументы того 
 * deferred, который был resolved не success.
 *           var d1 = new deferred, d2 = new deferred;
 *           d1.resolve(true, 1, 2, 3);
 *           d2.resolve(false, 'x', 'y', 'z')
 *           var A = deferred.all(d1, d2);
 *           A.on(false, function (a1, a2, a3)
 *           {
 *             // arguments is same as for d2 resolution
 *             // arguments is Arguments object ['x', 'y', 'z']
 *             // a1 == 'x'
 *             // a2 == 'y'
 *             // a3 == 'z'
 *           });

 *          .bunch = function bunch (variant, deferreds)
 *
 * Конструктор. Аналогичен deferred.all. Принимает variant и массив 
 * объектов deferred.
 * Создает новый объект deferred, который будет переведен в тот вариант,
 * если все переданные аргумены перейдут в это состояние. Если любой из 
 * аргументов будет переведен в иное состояние, то новый deffered будет 
 * переведен в состояние этого deffered.
 * deferred.all описываестя как bunch следующим образом:
 *          .bunch('success', deferreds); 
 * Аргументы имеют тотже механизм, что и в deferred.all.

 *          .isResolved
 * 
 * Свойство (ТОЛЬКО ДЛЯ ЧТЕНИЯ) объекта deferred, возврацающее флаг 
 * наличия состояния результата. Значение: true/false

 *          .resolution
 * 
 * Свойство (ТОЛЬКО ДЛЯ ЧТЕНИЯ) объекта deferred, возврацающее значение
 * состояния результата. Значение: null/variant

 *           ==ПЕРЕМЕННЫЕ==

 *           this._variants = {};  
 * 
 * Именованный массив(объект) обработчиков для различных вариантов сос-
 * тояний

 *           this._ever = []; 
 * 
 * Массив обработчиков выполняемых после обработчиков из массива 
 * this._variants (success, fail или любой пользовательский вариант).
 * Выполняются в независимости от значения variants, т.е. всегда.
 * Обработчики могут быть связаны с объектом Deferred через this, поэто-
 * му они могут использовать свойство resolution.

 *           this._resolution = null;
 * 
 * null или variant.  Состояние, что был разрешен объект deferred

 *           this._resolveArguments = null;
 * 
 * Массив аргументов передаваемых обработчику при вызове

 *           this._variants[variant]
 * 
 * Именованный массив (объект) функций-обработчиков для вариантов ре-
 * зультата //this._variants[variant]=handler

 *           ==ВНУТРЕННИЕ ФУНКЦИИ==

 *           _variantAliases(variant)
 * 
 * Проверка (защита от дурака) аргумента variant. 
 * Если variant === undefined,то устанавливается variant = 'success',
 * как значение поумолчанию.
 * Если variant === true или variant === false, соотсветственно variant 
 * = 'success' или 'fail'

 *           _on (variant, handler)
 * 
 * Добавляет фунцию-обработчика в массива обработчиков вариантов состоя-
 * ний результата this._variants[variant].
 * Проверяет существование массива this._variants[variant], если массив 
 * не создан, создает.

 *           _resolve (variant)
 * 
 * Переводит объект deferred в вариант состояния есть результат в соот-
 * ветствии с вариантом в переданной переменной variant.
 * В случае нескольких регистрируемых обработчиков для данного состояния
 * они будут вызваны. Любой обработчик регистрируемый в дальшейшем для 
 * данного состояния будет вызван немедленно.

 *           bunch_controller (i)
 * 
 * Проверяет состояние каждого переданного объекта deffereds, в случае 
 * если состояние совпадает с заданным через интерфейс  .bunch, то
 
 
*/




function deferred ()
{
	if (this instanceof deferred)
	{
		this._variants = {};
		this._ever     = [];

		this._resolution = null;
		this._resolveArguments = null;
	}
	else
	{
		return new deferred();
	}
}

deferred.prototype.success = function (handler)
{
	return this.on('success', handler);
};

deferred.prototype.fail = function (handler)
{
	return this.on('fail', handler);
};

deferred.prototype.on = function (variant, handler)
{
	variant = _variantAliases(variant);
	if (this.isResolved)
	{
		if (variant === this._resolution)
		{
			handler.apply(this, this._resolveArguments);
		}
	} else
	{
		_on.call(this, variant, handler);
	}
	return this;
};

function _variantAliases (variant)
{
	(variant === undefined) && (variant = 'success');
	((variant === true) || (variant === false)) && (variant = (variant ? 'success' : 'fail'));
	return variant;
}

function _on (variant, handler)
{
	this._variants[variant] || (this._variants[variant] = []);
	this._variants[variant].push(handler);
}

deferred.prototype.ever = function (handler)
{
	if (this.isResolved)
	{
		handler.apply(this, this._resolveArguments);
	} else
	{
		this._ever.push(handler);
	}
	return this;
};

deferred.prototype.resolve = function (variant)
{
	if (! this.isResolved)
	{
		_resolve.apply(this, arguments);
	}
	return this;
};

Object.defineProperty(deferred.prototype, 'isResolved', { get: function isResolved ()
{
	return this._resolution !== null;
}
});

Object.defineProperty(deferred.prototype, 'resolution', { get: function resolution ()
{
	return this._resolution;
}
});

function _resolve (variant)
{
	variant = _variantAliases(variant);

	this._resolution = variant;
	this._resolveArguments = Array.prototype.slice.call(arguments, 1);

	if (this._variants[variant] && this._variants[variant].length)
	{
		var handlers = this._variants[variant];
		for (var i = 0, L = handlers.length; i < L; i++)
		{
			handlers[i].apply(this, this._resolveArguments);
		}
	}

	if (this._ever.length)
	{
		for (var i = 0, handlers = this._ever, L = handlers.length; i < L; i++)
		{
			handlers[i].apply(this, this._resolveArguments);
		}		
	}

	this._variants = null;
	this._ever     = null;
}

/*

 [variant,] deferred[, fn_arg ]

 fn_arg - transform for arguments

defaults:
 - variant : all variants
 - fn_arg  : no transfrom, leave same arguments
*/
deferred.prototype.passthru = function (variant, deferred, fn_arg)
{
	if (variant instanceof this.constructor)
	{
		fn_arg   = deferred;
		deferred = variant;
		variant  = null;

		this.ever(passthru)
	}
	else
	{
		this.on(variant, passthru)
	}

	return this;

	function passthru ()
	{
		var args = Array.prototype.slice.call(arguments);
		if (fn_arg)
		{
			args = fn_arg.apply(this, args);
		}
		deferred.resolve.apply(deferred, [ this.resolution ].concat(args));
	}
};

deferred.prototype.log = function (name)
{
	return this.ever(logger);

	function logger ()
	{
		var log = console.log;
		switch (this.resolution)
		{
		case 'success':
			log = console.info;
			break;
		case 'fail':
			log = console.error;
			break;
		}
		var args = Array.prototype.slice.call(arguments);
		log.apply(console, [ name, this.resolution ].concat(args));
	}
};

deferred.all = function all (/* deferreds */)
{
	var deferreds = Array.prototype.concat.apply([], arguments)
	return this.bunch('success', deferreds);
}

deferred.bunch = function bunch (variant, deferreds)
{
	var
		_variant    = _variantAliases(variant),
		count       = deferreds.length,
		result      = new deferred;

	if (count)
	{
		var
			resCount    = 0,
			resMetaArgs = Array(count);

		for (var i = 0; i < count; i++)
		{
			deferreds[i].ever(bunch_controller.bind(deferreds[i], i));
		}
	}
	else
	{
		result.resolve(_variant);
	}

	return result;

	function bunch_controller (i)
	{
		var args = Array.prototype.slice.call(arguments, 1);
		if (this.resolution === _variant)
		{
			resCount++;
			resMetaArgs[i] = args;

			if (count === resCount)
			{
				result.resolve.apply(result, [ _variant ].concat(resMetaArgs));
			}
		}
		else
		{
			result.resolve.apply(result, [ this.resolution ].concat(args));
		}
	}
}

module.exports = deferred;

Примечания

Все объекты в javascript наследуют от Object, и потому имеют свойство prototype. Как правило, свойство prototype используется для предоставления базового набора функциональных возможностей классу объектов. Новые экземпляры объекта "наследуют" поведение прототипа, присвоенного этому объекту. Предположим, что нам требуется добавить в объект Array метод, который возвращает значение наибольшего элемента массива. Для этого объявляется функция, которая добавляется к объекту Array.prototype, а затем используется.

var result = function.apply(thisArg[, argsArray]);

Аргументы thisArg Задает значение this внутри функции. Если thisArg - null или undefined, то это будет глобальный объект. В ином случае, this будет равно Object(thisArg) (то есть thisArg, если thisArg уже объект, или String, Boolean или Number, если thisArg - примитивное значение соответствующего типа). Таким образом, при выполнении функции всегда соблюдается условие typeof this=='object'.

argsArray

Массив аргументов, с которыми будет вызвана функция, или null/undefined для вызова без аргументов.

Описание, примеры

Любую функцию в яваскрипт можно вызвать в контексте любого объекта. Используя apply, вы можете вызывать одну функцию в контексте множества различных объектов. Метод apply очень похож на call, за исключением передачи аргументов. В apply используется массив аргументов вместо списка именованных параметров. Используя apply, вы можете использовать литеральное объявление массива, например fun.apply(this, [name, value]). Вы также можете использовать arguments в качестве параметра argArray. Это избавляет от необходимости знать, с какими параметрами была вызвана исходная функция.

Object.defineProperty(object, propertyname, descriptor)

Добавляет свойство в объекте, или изменять атрибуты существующего свойства.

object

Обязательный параметр. Объект, для которого добавляется или изменяется свойство. Это может быть собственным объектом JavaScript (т.е.

определяемый пользователем объект или встроенный объект) или объектом DOM.

propertyname

Обязательный параметр. Строка, содержащая имя свойства.

descriptor

Обязательный параметр. Дескриптор свойства. Это может быть свойство данных или свойство доступа.