Что такое этот новый 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, rn2);
    callbacks.add(fn2);
    callbacks(2, 3);

    Также ещё у нового «конструктора» появилась возможность сразу же передавать начальные колбэки в параметрах, без лишнего вызова add().
    Ну и в работе: 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. Это и есть пример реального использования.
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 24
      0
      Спасибо, очень полезная штука.
        0
        Только сегодня нашел его в документации и не нашел на хабре статьи о таком полезном объекте, и тут ваша статья. Спасибо.
          +5
          А можно где-то посмотреть пример использования? Не развлечение с foo-bar'ами, а более или менее реальную задачу, в которой потребовался бы такой объект?
            0
            Любой пример с $.Deferred — это пример с $.Callbacks, т.к. первый основан на втором, а любой пример с $.ajax — это пример с $.Deferred :)
              0
              Добавил в конец статьи UPD по этому вопросу.
            +2
            Чисто субъективно, почему-то не хочется пользоваться ни Deferred, ни Callbacks в jQuery. На мой взгляд это необоснованное увеличение библиотеки, которая выигрывает у конкурентов как наиболее простой DOM-манипулятор.

            Кажется настало время серьезно посмотреть в сторону Prototype
              0
              всегда можно остановится и использовать версию 1.6 в своих проектах
                +1
                Никакой это не «наиболее простой DOM-манипулятор». Это уже давно весьма универсальная библиотека. На самом деле Deferred и Callbacks — это не так уж много лишнего кода. Если уж говорить о лишнем коде, то хотелось бы иметь версию jQ без поддержки старых браузеров IE 6-7.

                О Deferred и говорить нечего: очень часто облегчает жизнь. А Callback — уж если используется в коде самой jQ, почему бы и не вынести «наружу».

                Хотите простой DOM-манипулятор, возьмите AtomJS, например.
                  0
                  Тот же sizzlejs.com/ можно использовать
                    0
                    Кажется, там только отбирать элементы можно — манипулировать нельзя :)
                    0
                    Хотите простой DOM-манипулятор, возьмите AtomJS, например.

                    Меня вдохновляют такие сообщения =)
                      0
                      Ну а что, если взять только с плагином DOM, как я понял, то, получится как-раз DOM-манипулятор, весящий в десятки раз меньше jQuery :)
                        0
                        Да =)
                          0
                          Ок. Просто на секунду мне показался сарказм :)
                            +2
                            Нет, меня вдохновляют как автора фреймворка ;)
                              +1
                              Ну я знал, кто автор. Как же Вас не узнать то :)

                              P.S. Фреймворк действительно клевый. Правда активно использовать еще не доводилось, но по описанию мне нравится. Вот jQuery как-раз не хватает такой модульности, имхо.
                  +1
                  кажется, в последнее время разработчики jQuery занимаются тем, что переписывают функционал Dojo на свой лад
                  +2
                  Мне кажется, что в вашей статье не хватает живого реального примера странички с такой функциональностью, где требуется этот новый объект. Ваши примеры какие-то слишком академические.
                    0
                    Не спорю. Примеры брал из оф. доков, немного поправив для большей наглядности. Просто «на страничке» пример, скорее всего не придумать. Эта функциональность может потребоваться при написании собственных компонент или виджетов, но не при каких-то элементарных операциях с DOM.
                      +2
                      По своему предназначению это тот же MooTools.Class.Events, просто реализован не как примесь, а как делегат (а в этом что-то есть на самом деле). В мощных приложениях (не 200 строк мелкого кода на jQuery, а реально мощных, например игры) или в библиотеках данная функциональность очень полезна. Когда вы захотите сделать свой плагин для JQuery с большим количеством событий — вы сразу поймёте в чём прелесть кода из топика.
                        0
                        Добавил в конец статьи UPD по этому вопросу.
                          0
                          Я не совсем согласен с вашим UPD. То, что это часть $.ajax — никто не спорит. Имхо, людей интересует, как можно этот код применить на практике в своём проекте или плагине. А согласно upd можно посчитать, что смысла изучать этот кусок нету, ведь всё равно оно абстрагированно внутри ajax.
                            0
                            Я согласен, что людей должно интересовать, как это применить с пользой для себя (своих проектов). Мой UPD — это, в первую очередь, ответ тем, кто говорит, что разработчики беспричинно утежеляют вес библиотеки, добавляя сомнительные фичеры в код. Вот я и постарался объяснить, эти фичеры добавляют не просто так, как лишний код, а оптимизируют уже существующий код — т.о. вес библиотеки от них не сильно прибавляется, а пользы может оказаться уйма.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое