Свежие заметки :: Хранитель заметок

noteskeeper.ru

Персональный журнал для заметок Владимира Кузнецова

Свежие заметки

Самый маленький плагин для jQuery

Простейший плагин будет состоять только из одного выражения return this.

$.fn.noop = function () {
    return this;
};

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

$("div.title").noop().doSomething();

Практическая польза от него будет, когда плагин должен вызываться неявно.

$("div.title")[isVisible() ? "fadeTo" : "noop"](333, 0).doSomething();

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

На написание заметки вдохновил твит Артёма Поликарпова.

Блоговский движок под системой контроля версий Git

Все файлы блоговского движка я храню в Git. Я не стану всем и каждому советовать сразу переходить на систему контроля версий. Очевидно, что для её развертывания потребуется как минимум VPS, навыки администрирования и, собственно, умение пользоваться ею. Иначе все плюсы обратятся в сплошные минусы.

Однако, преодолев все эти трудности можно получить некоторые преимущества.

Редактирование темы и плагинов

Благодаря Git редактировать файлы можно как на домашнем компьютере, так и прямо на сервере. При этом точно знаю, что ничего не «случайно» не потеряется или перезатрется.

Если один и тот же файл был изменен в разных местах, то система сама попробует решить конфликт. А если её это окажется не под силу, то предоставит вам эту возможность.

Обновление версии движка

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

Чтобы сохранить свои исправления я выработал такую схему работы

Дерево коммитов Git

В одной ветке (wp31) я храню исходные файлы дистрибутива и все новые версии разворачиваю только туда. А свои правки делаю в основной ветке (master).

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

Развертывание изменений на сервере

Все действия сводятся к одной простой команде.

git pull

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

Резервное копирование

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

54307730.19286420.1324395758.c7d8ac3d6d9658c7c572ebaca98a9ceb

Исправляем структуру HTML5-документа в старых браузерах

В старых версиях Internet Explorer теги, которых нет в HTML 4.01, правильно будут стилизоваться только, если они предварительно созданы через JavaScript.

document.createElement("article");

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

Пример правильной структуры документа в Firefox 8

Правильная структура HTML5-документа

А так этот же документ будет разобран в Internet Explorer 6

Структура документа нарушена из-за новых тегов

Из-за нарушения общей структуры в таком документе становятся бессмысленными и каскадные правила CSS.

Наверное, сейчас верстка под IE6 с отключенным JS уже не является актуальнейшей проблемой. Тем не менее, поправить структуру документа можно с помощью дополнительных блочных или строчных (в зависимости от контекста) тегов на каждый тег, которого не было в спецификации HTML 4.01.

Исправленная структура документа

Навигация по ссылкам в списках

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

Список авторов статей в блоге

Каждый элемент в списке содержит имя, фотографию и краткую биографию автора. Имя и фотография обрамлены ссылками на архив статей этого автора.

<ul class="authors">
  <li class="author">
    <div class="author-userpic">
      <a class="author-archive-link" title="Vicki Moyes" href="/author/vicki/">
        <img class="author-pic photo" alt="Vicki Moyes" src="/img/users/vicki-moyes.jpg">
      </a>
    </div>
    <p class="author-title">
      <a href="/author/vicki/" class="author-title-archive-link">Vicki Moyes</a>
    </p>
    <p class="author-bio">Director of Moyes Gliders.
      Organizer of annual Forbes competition.</p>
  </li>
</ul>

Используем tabindex=-1

Элементы, обрамленные ссылками

При переключении фокуса с помощью клавиши «Tab» пользователь каждый раз сначала попадает на ссылку с фотографии, затем на ссылку с имени.

Проектируя интерфейс, первостепенно внимание уделяется пользователям, работающим с мышью или touch-интерфейсом. Кому-то из них привычнее и удобнее кликать по картинкам, а кто-то кликает только по явно обозначенным ссылкам. Поэтому удалить ссылку, обрамляющую фотографию, нельзя. Но зато можно сделать так, чтобы она не участвовала в последовательности переключения фокуса с клавиатуры. Для этого добавим ей атрибут tabindex="-1".

Переходы только по именам авторов

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

Одна ссылка

В этом примере два находящиеся рядом объекта (фотография и имя автора) должны вести на одну и ту же страницу. Технически их можно обернуть одной ссылкой. В HTML5 внутри строчного элемента a могут находиться как строчные, так и блочные элементы.

<a href="page.html">
    <div><img src="img1.jpg" alt="Page image"></div>
    <p>Page title</p>
</a>

Фрагмент кода валиден на 100%, только если используется в HTML5 документе.

Оповещение модулей через jQuery.Callbacks

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

  1. через интерфейс Observable или произвольные DOM-события;

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

  2. через глобальный объект;

    Экземпляры компонент ничего не знают друг о друге. Все события пересылаются через один и тот же объект.

  3. через объекты событий.

    В этом случае экземпляры компонент так же ничего не знаю друг о друге. А каждое событие имеет отдельный объект в глобальном пространстве имен.

Интерфейс Observable удобен тем, что события одного экземпляра ни как не пересекаются с событиями другого экземпляра одной и той же компоненты.

$("div.primary div.widget").on("update", function () {
    alert("update in primary block");
}

$("div.secondary div.widget").on("update", function () {
    alert("update in secondary block");
}

Два экземпляра одного и того же виджета, но расположенных в разных блоках генерируют разные события «update».

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

Создается глобальный объект, имеющий методы регистрации функции обратного вызова для события и отмены регистрации, а так же метод вызова зарегистрированных колбеков.

var Topic = (function () {
    var topics = {};
    return function (name) {
        var callbacks, method, topic;
        if (!name) {
            throw new Error("topic must be specified");
        }
        topic = topics[name];
        if (!topic) {
            callbacks = jQuery.Callbacks();
            topic = {
                publish: callbacks.fire,
                subscribe: callbacks.add,
                unsubscribe: callbacks.remove
            };
            topics[name] = topic;
        }
        return topic;
    };
}());

Замыкание содержит в себе список всех зарегистрированных событий. Для каждого из них создается менеджер очереди колбеков jQuery.Callbacks, который регистрирует и вызывает их.

Наружу возвращается объект с методами экземпляра jQuery.Callbacks, но именованными в соответствии с действиями, за которые они отвечают: subscribe, unsubscribe и publish.

Topic("archiveAdd").subscribe(function (id) {
    console.log("add file with id = " + id + " to archive");
});
Topic("archiveCreate").subscribe(function () {
    console.log("start archiving");
});

И в другой компоненте:

$(root).on("click", "li.file button.add", function () {
    Topic("archiveAdd").publish($(this).data("fileId"));
});
$(root).on("click", "button.create", function () {
    Topic("archiveCreate").publish();
});

В некоторых случаях может быть полезным для каждого события создать свой отдельный объект и использовать ссылку на него в компонентах

var EventObject = function () {
    var callbacks = jQuery.Callbacks();
    return {
        publish: callbacks.fire,
        subscribe: callbacks.add,
        unsubscribe: callbacks.remove
    };
};

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

Очередь функций обратного вызова jQuery.Callbacks

В jQuery 1.7 появился новый объект для создания и управления очередью колбеков – jQuery.Callbacks. Он был предложен на рассмотрение команде разработчиков еще 6 месяцев назад. Менеджер было успешно опробован и пошел в релиз. На его основе был переписан jQuery.Deferred, который в свою очередь лежит в основе системы колбеков jQuery.ajax.

Конструктор jQuery.Callbacks можно вызывать как с оператором new, так и без него. В результате будет создан объект с несколькими методами: add(), remove(), fire(), disable() и др.

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

Вызов конструктора с различными флагами

Очередь, созданная без флагов, ведет себя так как если бы это была, например, очередь используемая в событиях.

var c = $.Callbacks();
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

После выполнения этого кода в консоль будет выведено:

1: foo

1: bar

2: bar

Метод add() добавляет в очередь новую функцию обратного вызова, а метод fire() по очереди вызывает все функции из очереди с указанными параметрами.

Флаг once указывает на то, что метод fire() будет выполнен только один раз для такой очереди.

var c = $.Callbacks("once");
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

В этом коде в консоль выведутся результаты работы только первой функции.

1: foo

Флаг memory предписывает запоминать последнее значение, с которым был вызван метод fire() и автоматически вызывать вновь добавляемые колбеки с этим значением.

var c = $.Callbacks("memory");
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

Когда вызывается метод fire("foo"), то второго колбека еще нет в очереди. Но как только он будет добавлен, так сразу вызовется с параметром "foo".

1: foo

2: foo

1: bar

2: bar

Флаги можно комбинировать. Так одновременное использование once и memory даст результат аналогичный по свой сути работе jQuery.Deferred – вызов колбеков будет сделан только один раз, а новые колбеки сразу будут вызываться при их добавлении.

var c = $.Callbacks("once memory");
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

1: foo

2: foo

Если будет установлен флаг unique, то одна и та же функция обратного вызова в очередь будет добавлена только один раз.

И последний флаг stopOnFalse прерывает выполнение очереди, если какой-либо колбек вернет значение false.

Применение

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

Работа с тегами в Git

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

Создание тега

  1. Помечаем локальный коммит

    git tag 12345
    
  2. Отправляем его во внешний репозиторий

    git push origin 12345
    

Удаление тега из репозитория

  1. Удаляем тег локально

    git tag -d 12345
    
  2. Удаляем его во внешнем репозитории

    git push origin :refs/tags/12345
    
  3. Оповещаем каким-либо способом коллег, чтобы они сделали у себя команду из пункта 1

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

Перемещение тега на другой коммит

  1. Принудительно перезаписываем тег

    git tag -f 12345
    
  2. Отправляем во внешний репозиторий с принудительной перезаписью

    git push --force origin 12345
    
  3. Оповещаем коллег как и в случае удаления тега

Дополнение

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

git tag 12345 6ff87c4664981e4397625791c8ea3bbb5f2279a3

Отправить во внешний репозиторий все теги текущей ветки можно одной командой.

git push --tags

Оглавление HTML5 документа

Механизм построения оглавления (outline) страницы базируется на тегах, используемых при разметке документа. Например, главная страница моего блога может иметь такое оглавление:

  1. Свежие заметки :: Хранитель заметок
    1. Свежие заметки
      1. Убираем неоднородности на повторяющейся текстуре
      2. Deferred Object
      3. Возвращаясь к проверке типа данных
      4. Тень у полей ввода в мобильном Safari
        1. Очень простое решение
      5. Таблица без таблицы или display: table-cell для всех браузеров
    2. Навигация по сайту
      1. Облако тегов
      2. Категории
      3. Ссылки
    3. Архив заметок по месяцам

Только два типа тегов влияют на оглавление страницы: заголовки (h1-h6 и hgroup) и структурные теги (section, article, aside и nav).

Влияние заголовков на построение оглавления

<div>
    <h1>Свежие заметки :: Хранитель заметок</h1>
    <!-- начинаем новый подраздел -->
    <h2>Свежие заметки</h2>
    <!-- начинается статья -->
    <h3>Убираем неоднородности на повторяющейся текстуре</h3>
    <!-- статья про неоднородности и как их убрать -->
    <p>Повторяющиеся текстуры бумаги или ткани могут быть с
        разными неоднородностями по площади.</p>
    ...
    <!-- статья закончилась и начинается новая -->
    <h3>Deferred Object</h3>
    <!-- статья про отложенные объекты -->
    <p>Термин «отложенный объект» тесно связан с событийной
        моделью создания компонент и модулей приложения.</p>
    ...
    <h3>Возвращаясь к проверке типа данных</h3>
    ...
    <h3>Тень у полей ввода в мобильном Safari</h3>
    ...
    <h4>Очень простое решение</h4>
    ...
    <h3>Таблица без таблицы или display: table-cell
        для всех браузеров</h3>
    ...
    <!-- раздел свежих заметок закончился и начинается новый раздел -->
    <h2>Навигация по сайту</h2>
    <h3>Облако тегов</h3>
    <ul>
        <li class="rank-9">jquery</li>
        <li class="rank-6">ui</li>
        <li class="rank-4">utility</li>
        <li class="rank-4">hint</li>
        <li class="rank-4">css</li>
    </ul>
    <h3>Категории</h3>
    <ul>
        <li>Вёрстка</li>
        <li>JavaScript</li>
        <li>Apple</li>
        <li>Разное</li>
    </ul>
    <h3>Ссылки</h3>
    ...
    <h2>Архив заметок по месяцам</h2>
    ...
</div>

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

Учет разделов при построении оглавления

Добавим в разметку страницы некоторые структурные элементы.

<div>
    <h1>Свежие заметки :: Хранитель заметок</h1>
    <section>
        <h2>Свежие заметки</h2>
        <article>
            <h3>Убираем неоднородности на повторяющейся текстуре</h3>
            <p>Повторяющиеся текстуры бумаги или ткани могут быть с
                разными неоднородностями по площади.</p>
            ...
        </article>
        <article>
            <h3>Deferred Object</h3>
            <p>Термин «отложенный объект» тесно связан с событийной
                моделью создания компонент и модулей приложения.</p>
            ...
        </article>
        <article>
            <h3>Возвращаясь к проверке типа данных</h3>
            ...
        </article>
        <article>
            <h3>Тень у полей ввода в мобильном Safari</h3>
            ...
            <h4>Очень простое решение</h4>
            ...
        </article>
        <article>
            <h3>Таблица без таблицы или display: table-cell
                для всех браузеров</h3>
            ...
        </article>
    </section>
    <nav>
        <h2>Навигация по сайту</h2>
        <section>
            <h3>Облако тегов</h3>
            <ul>
                <li class="rank-9">jquery</li>
                <li class="rank-6">ui</li>
                <li class="rank-4">utility</li>
                <li class="rank-4">hint</li>
                <li class="rank-4">css</li>
            </ul>
        </section>
        <section>
            <h3>Категории</h3>
            <ul>
                <li>Вёрстка</li>
                <li>JavaScript</li>
                <li>Apple</li>
                <li>Разное</li>
            </ul>
        </section>
        <section>
            <h3>Ссылки</h3>
            ...
        </section>
    </nav>
    <nav>
        <h2>Архив заметок по месяцам</h2>
        ...
    </nav>
</div>

Фактически структура оглавления ни сколько не поменялась. Но теперь разделы заданы явно с помощь тегов section, article, nav.

Одним из широко обсуждаемых нововведений HTML5 стало то, что в документе разрешено использовать несколько тегов h1. Из-за того, что деление на разделы происходит явно, внутри каждого раздела можно формировать свою структуру из заголовков.

В спецификации HTML5 указано:

Sections may contain headings of any rank, but authors are strongly encouraged to either use only h1 elements, or to use elements of the appropriate rank for the section’s nesting level.

Разделы могут содержать заголовки любого уровня. Но авторам настоятельно рекомендуется использовать только элементы h1 или элементы, соответствующие уровню вложенности раздела.

Лично я не стал бы использовать несколько элементов h1 до тех пор пока такой принцип разметки не получит поддержку в поисковых системах и читалках с экрана. Гораздо безопаснее структурировать документ, с использованием тегов явной разметки секций и заголовки такого же уровня, как если бы секции задавались неявно. С появлением полной поддержки вам не придется менять разметку страницы, так как она уже будет готова для обработки другим алгоритмом.

Смешанный подход

Неявные разделы могут появляется внутри явных разделов (но не наоборот).

<div>
    <h1>Свежие заметки :: Хранитель заметок</h1>
    <section>
        <h2>Свежие заметки</h2>
        <article>
            <h3>Тень у полей ввода в мобильном Safari</h3>
            ...
            <h4>Очень простое решение</h4>
            ...
        </article>
    </section>
</div>

Этот фрагмент документа, очевидно, выдаст следующую структуру:

  1. Свежие заметки :: Хранитель заметок
    1. Свежие заметки
      1. Тень у полей ввода в мобильном Safari
        1. Очень простое решение

Заголовок h4 образует неявный раздел внутри статьи, заданной явным образом тегом article.

Разделы без названия

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

Оставлять же без названия разделы section и article не стоит. Наличие такого раздела, например, может быть связано с не правильным выбором тега между div, section и article. Если для блока нельзя придумать заголовок, то его стоит просто разметить с помощью тега div.

Кстати говоря, элемент body так же получает название из ближайшего заголовка, а не из содержимого тега title, как можно предположить. И если такого заголовка не окажется, то документ останется без названия. Это серьезная проблема и таких ситуаций следует избегать, планируя разделы документа.

<body>
    <article>
        <h1>Blog post title</h1>
        <p>Blog post content</p>
    </article>
</body>

Такой документ останется без названия, так как появился явный раздел, размеченный тегом article. Чтобы решить эту проблему достаточно перед article поставить еще один тег h1 с аналогичным содержимым. Этот прием никак не повиляет на поисковую оптимизацию, так как заголовки будут одинаковыми (или почти одинаковыми).

hgroup

Элемент hgroup может содержать только заголовки и его назначение – это убрать все заголовки из оглавления кроме заголовка самого высокого уровня.

Инструменты и документация

Убираем неоднородности на повторяющейся текстуре

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

Допустим у нас имеется повторяющаяся текстура. Она прекрасно стыкуется по краям.

Повторяющаяся текстура

Если попробовать наложить её саму на себя, то границы будут заметны с первого взгляда. Перепады по яркости специально сделаны такими отчетливыми, чтобы продемонстрировать, что и их можно легко устранить.

Повторяющаяся текстура не стыкуется при наложении

Скопируем слой и применим к нему фильтр «Gaussian Blur». Параметры фильтра нужно выбрать такими, чтобы исчезли все детали текстуры и остались только паразитные пятна. В моем случае радиус размытия был равен 10 (чем он будет больше, тем больше деталей останется в результате). Понизим немного яркость этому слою. Это удобно сделать с помощью Adjustment Layer, так как аналогичную операцию придётся сделать ещё раз. Поместим оба слоя в отдельную группу.

Размытая текстура

Теперь создадим новый слой и зальем его сплошным цветом, который хотим получить в результате. Так же поместим его в отдельную группу и добавим туда копию корректирующего яркость слоя. Теперь первой группе с размытой текстурой назначаем режим смешивания «Subtract» (вычитаем его из оригинальной текстуры), а второй группе со сплошным цветом – «Linear Dodge» (добавляем его к разнице).

Слои и настройки

В результате получим оригинальную текстуру, но без неоднородностей.

Текстура без неоднородностей

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

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

Таким же образом можно исправить и темную текстуру. Только нужно внести два изменения:

  1. Группы с размытым слоем и сплошным цветом меняем местами — сначала добавляем, а потом вычитаем. Это важно сделать из-за зануления отрицательных значений.
  2. Корректирующие слои становятся не нужны, так как при их использовании есть вероятность потерять детали в самых темных участках.

Deferred Object

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

Для полного понимания приведу пример фабрики отложенных объектов Дугласа Крокфорда из его презентации «Act III: Function the Ultimate»

function make_promise() {
    var status = 'unresolved',
        outcome,
        waiting = [],
        dreading = [];

    function vouch(deed, func) {
        switch (status) {
            case 'unresolved':
                (deed === 'fulfilled' ? waiting : dreading).push(func);
                break;
            case deed:
                func(outcome);
                break;
        }
    }

    function resolve(deed, value) {
        if (status !== 'unresolved') {
            throw new Error('The promise has already been resolved:' + status);
        }
        status = deed;
        outcome = value;
        (deed == 'fulfilled' ? waiting : dreading)
            .forEach(function (func) {
                try {
                    func(outcome);
                } catch (ignore) {}
            });
        waiting = null;
        dreading = null;
    }

    return {
        when: function (func) {
            vouch('fulfilled', func);
        },
        fail: function (func) {
            vouch('smashed', func);
        },
        fulfill: function (value) {
            resolve('fulfilled', value);
        },
        smash: function (string) {
            resolve('smashed', string);
        },
        status: function () {
            return status;
        }
    };
}

Состояние объекта можно изменить только один раз каким-либо методом: fulfill или smash. После этого будет выполнены функции, которые были зарегистрированы методами when и fail, соответственно.

Стоит заметить, что отложенные объекты применимы только к разовым событиям. После того, как объект был установлен в какое-то состояние, изменить его уже нельзя.

Эту концепцию команда разработчиков jQuery реализовала в своем фреймворке и начала использовать при работе с AJAX. Далее я буду использовать именно эту реализацию.

Рассмотрим пример приложения, с использованием отложенных объектов: «По клику на элемент появляется селектор с мероприятиями, содержимое которого загружается из внешнего сервиса. Когда пользователь выбирает мероприятие, оно добавляется в список».

Начнем с функции, отслеживающей клики. Она ничего не знает о селекторе и списке кроме того, что они существуют и поддерживают интерфейс jQuery.Deferred.

$(function () {
    // отслеживаем нажатие элемента
    $("span.show-selector").bind("click", function () {
        var ele = $(this), select;
        // ничего не делаем, если уже есть блок с селектором
        if (ele.hasClass("busy")) {
            return;
        }
        ele.addClass("busy");

        // создаем селектор
        select = buildSelect();
        // передаем отложенный объект селектора в функцию обновления списка
        updateList(select);

        // как только в селекторе было выбрано мероприятие снимаем флаг
        select.always(function () {
            ele.removeClass("busy");
        });
    });
});

Функция, создающая селектор знает, что список мероприятия нужно откуда-то получить, но не вдается в реализацию этого процесса. Она рисует селектор и возвращает отложенный объект, который изменит свой статус после того как пользователь сделает свой выбор. Статус так же изменится, если при получении списка возникнет ошибка.

function buildSelect() {

    var select = $.Deferred(),
        fetcher = fetchEvents();

    // когда данные о мероприятиях будут получены, рисуем селектор
    fetcher.done(function (data) {
        var ele = [], cache = {};

        $.each(data.query.results.event, function (index, upcomingEvent) {
            ele.push('<option value="' + upcomingEvent.id + '">');
            ele.push(upcomingEvent.name);
            ele.push('</option>');
            cache[upcomingEvent.id] = upcomingEvent;
        });

        $("p.title").append('<div id="upcoming-events">' +
            '<select>' + ele.join('') + '</select>' +
            '<button class="btn-done">Done</button>' +
            '<button class="btn-cancel">Cancel</button>' +
            '</div>');

        // отслеживаем нажания на кнопки, чтобы выбрать
        // мероприятие или отменить выбор
        $("#upcoming-events").delegate("button", "click", function () {
            var upcomingEvent;
            if ($(this).hasClass("btn-done")) {
                // мероприятие было выбрно
                upcomingEvent = cache[$("#upcoming-events select").val()];
                // меняем состояние отложенного объекта
                // и передаем подписчикам информацию о мероприятии
                select.resolve(upcomingEvent);
            } else if ($(this).hasClass("btn-cancel")) {
                // выбор был отменен пользователем
                select.reject("user");
            }
        });

    });

    // ошибка при получении списка мероприятий
    fetcher.fail(function () {
        // отменяем выбор из-за сбоя связи с сервисом YQL
        select.reject("network");
    });

    // удаляем селектор, когда было выбрано какое-то мероприятие
    // или выбор был отменен по каким-то причинам
    select.always(function () {
        $("#upcoming-events").remove();
    });

    // возвращаем отложенный объект селектора
    return select;
}

Внутри она сама подписывается на изменение статуса для того, чтобы удалить селектор со страницы. Так как в общем случае deferred object предполагает двустороннюю коммуникацию, то ничего не мешает отменить выбор из вне.

Функция получения списка мероприятий запрашивает их через YQL. Сейчас она реализована в самом простом варианте без кеширования полученных результатов. Но благодаря тому, что используются унифицированный deferred object, а не функции обратного вызова в ajax-запросе, функциональность можно легко дополнить.

function fetchEvents() {
    // получаем список мероприятий через сервис YQL
    var jqXHR = $.ajax({
        url: "http://query.yahooapis.com/v1/public/yql",
        data: {
            q: 'select * from upcoming.events where tags="birthday"',
            format: 'json'
        },
        dataType: 'jsonp'
    });
    // возвращаем отложенный объект ajax-загрузки
    return jqXHR;
}

И наконец последняя функция, рисующая выбранное мероприятие, просто подписывается на событие выбора.

function updateList(select) {
    // в селекторе было выбрано какое-то мероприятие
    select.done(function (upcomingEvent) {
        // получаем элемент списка или создаем его
        var list = $("dl.list");
        if (!list.length) {
            list = $('<dl class="list"></dl>').appendTo("body");
        }

        // добавляем новую запись о мероприятии в список
        list.append('<dt>' + upcomingEvent.name + '</dt>' +
            '<dd>' + upcomingEvent.description + '</dd>');
    });
}

Посмотреть работу примера.

В заключении хочу сказать, что deferred object не представляет собой замену обычным событиям, а скорее является частным случаем, когда допустимы следующие особенности:

  • ожидаемое событие случается только один раз;
  • подписчики могут добавлять функции обратного вызова даже после того как событие случилось.

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