Термин «отложенный объект» тесно связан с событийной моделью создания компонент и модулей приложения. По сути, представляет собой интерфейс, с помощью которого можно в одном модуле подписываться на события другого модуля. Основной отличительной чертой 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 не представляет собой замену обычным событиям, а скорее является частным случаем, когда допустимы следующие особенности:
- ожидаемое событие случается только один раз;
- подписчики могут добавлять функции обратного вызова даже после того как событие случилось.
Так же отложенный объект имеет преимущества перед функциями обратного вызова для связи модулей и компонент, так как позволяет создавать цепочки из таких функций.