пятница, 6 января 2012 г.

Что такое этот новый jQuery.Callbacks Object

В не столь давно вышедшей версии jQuery 1.7 появился новый объект Callbacks, о котором сегодня и пойдёт речь.
В официальной документации jQuery.Callbacks описан, как многоцелевой объект, представляющий собой список функций обратного вызова (callbacks - далее просто колбэков) и мощные инструменты по управлению этим списком.

Я просматривал возможности этого объекта, когда он был ещё только в разработке, и надо сказать, что возможностей у него изначально было немного больше, чем осталось в релизной версии. Например, сейчас отсутствует возможность создания очереди (queue) колбэков, которые вызываются по одному на каждый вызов fire(). Видимо, команда jQuery, решила немного подсократить код, убрав "ненужные/редкоиспользуемые" возможности, чтобы сэкономить в весе библиотеки. Это маленький экскурс в историю Callbacks, но далее я буду описывать только доступные сейчас функции и в конце напишу небольшое возможное улучшение этого объекта.

Назначение

Прежде чем приступить к подробному изучению этого нового объекта jQuery.Callbacks, хочу остановиться на том, для чего же вообще нужен этот объект. Довольно часто в JavaScript коде используются колбэки - функции, которые вызываются при наступлении некоторого события, например, после завершения какого-то действия, самым ярким примером может послужить запрос AJAX. И при этом часто возникает потребность вызвать не одну функцию, а сразу несколько (заранее неизвестно сколько, может быть пару, может быть пару десятков, а может вообще ни одной) - это известный и простой паттерн "Наблюдатель". И вот для таких случаев и оказывается полезен рассматриваемый объект jQuery.Callbacks. В самом jQuery этот объект используется (начиная с версии 1.7) внутри jQuery.Deferred и jQuery.ajax. Также авторы jQuery сделали этот объект общедоступным и задокументировала его, чтобы другие разработчики могли его использовать при реализации собственных компонентов.

Конструктор: jQuery.Callbacks(flags)

Вызовом конструктора создаётся объект callbacks, который имеет ряд методов для управления списком колбэков.
Параметр flags необязательный и позволяет задать параметры работы объекта, возможные значения параметра мы рассмотрим ниже.
var callbacks = $.Callbacks();
К созданному объекту callbacks мы сможем теперь добавлять функции-колбэки в список, удалять их, вызывать, снова вызывать (если это не было запрещено при создании объекта), проверять статус объекта (был ли уже вызов или ещё нет) с помощью таких методов объекта, как add(), remove(), fire() и пр. Выполняются колбэки, кстати, в порядке их добавления в список.

Отмечу, что это не "настоящий" конструктор экземпляра класса, поэтому использовать оператор new при его вызове не требуется (и даже бессмысленно).

По этой причине не получится проверить, является ли объект экземпляром Callbacks, способом, стандартным для JS:
if (obj instanceof $.Callbacks) {
    obj.add(fn);
}
Выражение под if всегда возвращает false. Но можно положиться на один из известных методов этого объекта (или сразу на несколько), например, можно проверить так:
if (obj.fire) {
    obj.add(fn);
}

На самом деле, внутри этой функции создаётся обычный JS-объект с определённым набором методов, которые опираются на своё замыкание - это довольно распространённый в JavaScript способ задания приватных (private) переменных, недоступных вне этого псевдоконструктора.

Также, благодаря такому псевдоконструктору, методы этого объекта никак не зависят от контекста вызова - объекта, которому они принадлежат, а это значит, что их можно смело присваивать в свойства другого объекта, не заботясь о смене контекста, - всё по прежнему будет работать корректно. Это справедливо для всех методов, кроме fire, он как раз таки зависит от контекста, но использует его в качестве контекста выполнения колбэков из списка, т.е. этот метод в ряде случаев не просто можно, а именно нужно присваивать свойствам другого объекта со сменой контекста. Например:
var c = $.Callbacks(), obj = {};
obj.register = c.add;
obj.register(function() { console.log('fired'); });
c.fire();
// output: 'fired'

Флаги

Примечание: далее по тексту под словами "вызов метода fire()" понимается вызов выполнения колбэков из списка в том числе и методом fireWith().
Параметр конструктора flags - это строка, в которой через пробел можно указать флаги - опции, в соответствии с которыми будет работать созданный объект callbacks. Поддерживаются такие флаги:

once - указывает, что список колбэков может быть выполнен только единожды, второй и последующие вызовы метода fire() будут безрезультатны (как это сделано в объекте deferred), если этот флаг не указан, то можно несколько раз вызывать метод fire().

memory - указывает, что необходимо запоминать параметры последнего вызова метода fire() (и выполнения колбэков из списка) и немедленно выполнять добавляемые колбэки с соответствующими параметрами, если они добавляются уже после вызова метода fire() (как это сделано в объекте deferred).

unique - указывает, что каждый колбэк может быть добавлен в список только один раз, повторная попытка добавить колбэк в список ни к чему ни приведёт.

stopOnFalse - указывает, что нужно прекратить выполнение колбэков из списка, если какой-то из них вернул false, в пределах текущей сессии вызова fire(). Следующий вызов метода fire() начинает новую сессию выполнения списка колбэков, и они будут выполняться опять до тех пор, пока один из списка не вернёт false либо пока не закончатся.

Методы

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

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

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

callbacks.has(callback) returns: boolean - проверяет, есть ли указанная функция в списке колбэков.

callbacks.empty() returns: callbacks - очищает список колбэков.

callbacks.disable() returns: callbacks - "отключает" объект callbacks, все действия с ним будут безрезультатны. При этом перестают работать вообще все методы: add - ни к чему не приводит, has - всегда возвращает false и пр.

callbacks.disabled() returns: boolean - проверяет, отключен ли объект callbacks, после вызова disable() будет возвращать true.

callbacks.lock() returns: callbacks - фиксирует текущее состояние объекта callbacks относительно параметров и состояния выполнения списка колбэков. Этот метод актулен при использовании флага memory и предназначен для блокирования только последующих вызовов fire(), в остальных случаях он равносилен вызову disable().
Детально этот метод работает так: если флаг memory не указан или ещё ни разу не был вызван метод fire() или последняя сессия выполнения колбэков была прервана возвратом false одним из них, то вызов lock() равносилен вызову disable() (именно он и вызывается внутри) и вызов disabled() в таком случае вернёт true, иначе будут заблокированы только последующие вызовы fire() - они не приведут ни к выполнению колбэков, ни к изменению параметров выполнения добавляемых колбэков (при наличии флага memory).

callbacks.locked() returns: boolean - проверяет, зафиксирован ли объект callbacks методом lock(), также верёт true после вызова disable().

callbacks.fireWith( [context] [, args] ) returns: callbacks - запускает выполнение всех колбэков в списке с указанным контекстом и аргументами. context - указывает контекст выполнения колбэка (объект, доступный через this внутри функции). args - массив (именно массив) аргументов, передаваемых в колбэк.

callbacks.fire( arguments ) returns: callbacks - запускает выполнение всех колбэков в списке с контекстом вызова и аргументами этого метода. arguments - список аргументов (не массив, как в методе fireWith()). Т.е. контекстом вызова и аргументами колбэков будут контекст и аргументы метода fire().

Пример, как можно эквивалентно запустить исполнение колбэков с одинаковыми параметрами и контекстом:
var callbacks = $.Callbacks(),
    context = { test: 1 };
callbacks.add(function(p, t) { console.log(this.test, p, t); });

callbacks.fireWith(context, [ 2, 3 ]);
// output: 1 2 3

context.fire = callbacks.fire;
context.fire(2, 3);
// output: 1 2 3

Колбэки из списка выполняются в том порядке, в котором они в этот список добавлялись. После выполнения колбэков при указанном флаге once список будет очищен, а если при этом не указан флаг memory или выполнение колбэков было прервано возвратом false, то объект callbacks будет отключен методом disable().

Примеры

Давайте посмотрим как работают флаги на примерах. Во всех примерах используются такие функции:
function fn1( value ){
    console.log( value );
}

function fn2( value ){
    fn1("fn2 says:" + value);
    return false;
}

$.Callbacks():
var callbacks = $.Callbacks();
callbacks.add( fn1 );
callbacks.fire( "foo" );
callbacks.add( fn2 );
callbacks.add( fn1 );
callbacks.fire( "bar" );
callbacks.remove( fn2 );
callbacks.fire( "foobar" );
console.log(callbacks.disabled());

/*
output: 
foo
bar
fn2 says:bar
bar
foobar
foobar
false
*/
Никакие флаги не указали - вполне ожидаемое поведение.

$.Callbacks('once'):
var callbacks = $.Callbacks( "once" );
callbacks.add( fn1 );
callbacks.fire( "foo" );
callbacks.add( fn2 );
callbacks.add( fn1 );
callbacks.fire( "bar" );
callbacks.remove( fn2 );
callbacks.fire( "foobar" );
console.log(callbacks.disabled());

/*
output: 
foo
true
*/
Тут всё понятно - один раз выполнили, что было, далее ничего не происходит, что ни делали, т.к. список уже отключен.

$.Callbacks('memory'):
var callbacks = $.Callbacks( "memory" );
callbacks.add( fn1 );
callbacks.fire( "foo" );
callbacks.add( fn2 );
callbacks.add( fn1 );
callbacks.fire( "bar" );
callbacks.remove( fn2 );
callbacks.fire( "foobar" );
console.log(callbacks.disabled());

/*
output: 
foo
fn2 says:foo
foo
bar
fn2 says:bar
bar
foobar
foobar
false
*/
Здесь, вроде, тоже ничего сложного - после первого выполнения каждое добавление колбэка вызывает его немедленное выполнение, а потом снова вызываем выполнение всего списка. При этом одну функцию мы добавили в список дважды - она дважды и срабатывает.

$.Callbacks('unique'):
var callbacks = $.Callbacks( "unique" );
callbacks.add( fn1 );
callbacks.fire( "foo" );
callbacks.add( fn2 );
callbacks.add( fn1 );
callbacks.fire( "bar" );
callbacks.remove( fn2 );
callbacks.fire( "foobar" );
console.log(callbacks.disabled());

/*
output: 
foo
bar
fn2 says:bar
foobar
false
*/
А в этом случае повторное добавление функции fn1 было проигнорировано.

$.Callbacks('stopOnFalse'):
var callbacks = $.Callbacks( "stopOnFalse" );
callbacks.add( fn1 );
callbacks.fire( "foo" );
callbacks.add( fn2 );
callbacks.add( fn1 );
callbacks.fire( "bar" );
callbacks.remove( fn2 );
callbacks.fire( "foobar" );
console.log(callbacks.disabled());

/*
output: 
foo
bar
fn2 says:bar
foobar
foobar
false
*/
Колбэк fn2 прерывает цепочку выполнения, т.к. возвращает false.

Это простые примеры, а теперь давайте попробуем поиграться с комбинациями флагов - будет немного интереснее:

$.Callbacks('once memory'):
var callbacks = $.Callbacks( "once memory" );
callbacks.add( fn1 );
callbacks.fire( "foo" );
callbacks.add( fn2 );
callbacks.add( fn1 );
callbacks.fire( "bar" );
callbacks.remove( fn2 );
callbacks.fire( "foobar" );
console.log(callbacks.disabled());

/*
output: 
foo
fn2 says:foo
foo
false
*/
Видим, что сработали только первый fire() и добавление новых колбэков привело к их немедленному выполнению с параметрами первого fire().

$.Callbacks('once memory unique'):
var callbacks = $.Callbacks( "once memory unique" );
callbacks.add( fn1 );
callbacks.fire( "foo" );
callbacks.add( fn2 );
callbacks.add( fn1 );
callbacks.fire( "bar" );
callbacks.remove( fn2 );
callbacks.fire( "foobar" );
console.log(callbacks.disabled());

/*
output: 
foo
fn2 says:foo
foo
false
*/
Здесь результат тот же, несмотря на то, что мы указали флаг unique и дважды добавляем fn1, - второй раз добавление этой функции в список сработало, потому что при указанном флаге once после выполнения колбэков список очищается, а флаг memory указывает, что последующие добавления колбэков будут приводить к их немедленному выполнению без помещения в список, а так как список пуст - то добавление любой функции всегда уникально. Но этот флаг сыграет свою роль при попытке добавить за раз несколько колбэков, среди которых есть дублирующиеся, если в предыдущем коде изменить 4-ю строку как показано ниже, то fn2 всё равно выполнена будет только один раз (а без флага unique была бы выполнена три раза):
callbacks.add( fn2, fn2, fn2 );

$.Callbacks('once memory stopOnFalse'):
var callbacks = $.Callbacks( "once memory stopOnFalse" );
callbacks.add( fn1 );
callbacks.fire( "foo" );
callbacks.add( fn2 );
callbacks.add( fn1 );
callbacks.fire( "bar" );
callbacks.remove( fn2 );
callbacks.fire( "foobar" );
console.log(callbacks.disabled());

/*
output: 
foo
fn2 says:foo
true
*/
Возврат false заблокировал все дальнейшие выполнения колбэков и при наличии флага once вообще привёл к отключению объекта callbacks.

Я не буду рассматривать все возможные комбинации флагов, я постарался выбрать наиболее интересные (не совсем простые) и объяснить поведение callbacks. Остальные комбинации можно протестировать самостоятельно, например, воспользовавшись заготовкой: http://jsfiddle.net/zandroid/JXqzB/

Обещанное улучшение

Улучшение, конечно, совсем не обязательное и даже, может быть, в какой-то степени надуманное, не судите строго.
Идея улучшения в том, чтобы опустить вызов метода fire(), а вместо этого сам объект callbacks использовать как функцию. Для этого пишем такую функцию:
(function($, undefined){
    $.FCallbacks = function(flags, fns) {
        var i = $.type(flags) === 'string' ? 1 : 0,
            callbacks = $.Callbacks(i ? flags : undefined);
        callbacks.add(Array.prototype.slice.call(arguments, i))
        return $.extend(callbacks.fire, callbacks, { fcallbacks: true });
    };
})(jQuery);
И без лишних слов посмотрим пример использования:
function fn1(p1, p2) { console.log('fn1 says:', this, p1, p2); }
function fn2(p1, p2) { console.log('fn2 says:', this, p1, p2); }
var callbacks = $.FCallbacks('once', fn1, fn2);
callbacks.add(fn2);
callbacks(2, 3);
Также ещё у нового "конструктора" появилась возможность сразу же передавать начальные колбэки в параметрах, без лишнего вызова add().
Ну и в работе: http://jsfiddle.net/zandroid/RAVtF/

Всех с наступившими праздниками, спасибо за внимание.



UPD в ответ на комментарии на Хабре:
Судя по комментариям, я всё таки зря опустил информацию о том, как этот объект используется внутри jQuery. Комментарии по поводу "сделали Deferred - а ведь это двойник такого-то метода в таком-то фреймворке" или "зачем нужен этот Callbacks - только утяжеляет вес библиотеки jQuery, а реальных применений не придумывается" - это, на мой взгляд, комментарии, не разбираясь в сути вопроса. Ниже я этот момент и хочу пояснить.

Реальное использование

Callbacks на самом деле теперь используется очень многими пользователями jQuery 1.7+ и был реализован командой разработчиков не просто, потому что им захотелось сделать новый фичер. Смотрите, цепочка и логика этого вопроса довольно проста:

В библиотеке был реализован метод $.ajax(), который по своей природе не что иное, как надстройка над неким Deferred - разработчики улучшили код, вынесли его отдельно от основного кода $.ajax() (для возможности повторного использования и упрощения тестирования) и решили, а почему бы не опубликовать этот код (дать доступ пользователям библиотеки к нему и задокументировать его) - получился $.Deferred.

В свою очередь $.Deferred - это изначально два (done() и fail()), а теперь три (+ ещё progress()) надстройки над Callbacks, который был сделан как внутренний код $.Deferred. И снова, разработчики улучшили и отделили этот код от $.Deferred, реализовав последний через $.Callbacks (кстати, source-код $.Deferred стал при этом намного понятнее и читабельнее).

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

Progg it