Статьи

Обещания и отложенные объекты в jQuery и AngularJS

 Серия статей о фьючерсах / обещаниях без JavaScript не будет полной. Фьючерсы (более часто называемые обещаниями на земле JS) повсеместно распространены в JavaScript до такой степени, что мы их почти не узнаем. AJAX, тайм-ауты и весь Node.JS построены на основе асинхронных обратных вызовов. Вложенные обратные вызовы (как мы увидим через секунду) настолько сложны для отслеживания и поддержания, что термин ада обратного вызова был придуман. В этой статье я объясню, как обещания могут улучшить читаемость и модульность вашего кода.

Представляем объект обещания

Давайте рассмотрим первый, самый простой пример с использованием AJAX и
$.getJSON()вспомогательного метода:

$.getJSON('square/3', function(square) {
    console.info(square);
});

square/3является ресурсом AJAX, который дает
9(
3 квадрата ). Я предполагаю, что вы знакомы с AJAX и понимаете, что регистрация обратных вызовов
9будет выполняться асинхронно, как только ответ поступит с сервера. Это так просто, но быстро становится громоздким, когда вы начинаете вложение, создание цепочки и хотите обрабатывать ошибки:

$.getJSON('square/3', function(threeSquare) {
    $.getJSON('square/4', function(fourSquare) {
        console.info(threeSquare + fourSquare);
    });
});
 
$.ajax({
    dataType: "json",
    url: 'square/10',
    success: function(square) {
        console.info(square);
    },
    error: function(e) {
        console.warn(e);
    }
});

Внезапно бизнес-логика глубоко погрузилась во вложенные обратные вызовы (на самом деле это все еще неплохо, но на практике это гораздо хуже). Есть еще одна проблема с обратными вызовами — практически невозможно написать чистые, повторно используемые компоненты, когда вам нужны обратные вызовы. Например, я хотел бы инкапсулировать вызов AJAX с помощью
function square(x)полезной утилиты. Но как «вернуть» результат? Обычно разработчики просто требуют функции обратного вызова, которая при условии, что, безусловно , не чище
function square(x, callbackFun). К счастью, мы знаем модель будущего / обещания, и jQuery (начиная с версии 1.5 с дальнейшими улучшениями в версии 1.8) реализует ее с помощью предложения
CommonJS Promises / A API :

function square(x) {
    return $.getJSON('square/' + x);
}
 
var promise3 = square(3);
//or directly:
var promise3b = $.getJSON('square/3');

Что
square()или точнее
$.getJSON()возвращает? Вызов не синхронный — мы возвращаем объект обещания! Мы «обещаем», что результат будет доступен через некоторое время. Как мы можем получить этот результат? В Java и Scala блокирование
Futureне рекомендуется. В jQuery это даже невозможно (по крайней мере, нет API). Но у нас есть чистый API для регистрации обратных вызовов:

promise3.done(function(threeSquare) {
    console.info(threeSquare);
});
promise3.done(function() {
    console.debug("Done");
});
promise3.done(function(threeSquare) {
    $('.result').text(threeSquare);
});

Так в чем же разница? Прежде всего, мы
возвращаем что-то, а не выполняем обратный вызов, что делает код более читабельным и приятным для просмотра. Во-вторых, мы можем зарегистрировать столько несвязанных обратных вызовов, сколько мы хотим, и все они выполняются по порядку. Наконец,
promiseобъект запоминает результат, поэтому даже если мы зарегистрируем обратный вызов
после того, как обещание было разрешено (ответ получен), оно все равно будет выполнено. Но это только верхушка айсберга. Позже мы увидим различные методы и шаблоны, которые появляются с обещаниями в JavaScript.

Объединяя обещания

Прежде всего вы можете легко «дождаться» двух или более произвольных обещаний:

var promise3 = $.getJSON('square/3');
var promise5 = $.getJSON('square/5');
 
$.when(promise3, promise5).done(
    function(threeSquare, fiveSquare) {
        console.info(threeSquare, fiveSquare);
    }
);

Нет вложенности или состояния. Просто получите два обещания и дайте библиотеке уведомить нас, когда оба результата будут доступны. Обратите внимание, что
$.when(promise3, promise5)возвращается еще одно обещание, так что вы можете в дальнейшем связать и преобразовать его. Одним из недостатков
$.whenявляется то, что он не принимает (распознает) массив обещаний. Но JavaScript достаточно динамичен, чтобы легко его обойти:

var promises = [
    $.getJSON('square/2'),
    $.getJSON('square/3'),
    $.getJSON('square/4'),
    $.getJSON('square/5')
];
 
$.when.apply($, promises).done(function() {
    console.info(arguments);
});

Если вам трудно следовать:

  1. Каждый $.getJSON()возвращает объект обещания, таким образом, promisesявляется массивом обещаний ( дух! )
  2. Each resolved promise is passed as a separate argument so we must use arguments pseudo-array to capture them all.
  3. done() callback is executed when all promises are resolved (last AJAX call returns) but promises can come from any source, not necessarily from AJAX request (read further about Deferred object)
  4. $.when() has exact same semantics as Futures.allAsList() in Guava and Future.sequence() in Akka/Scala.
  5. (sidenote) Initiating several AJAX calls at the same time is not necessarily the best design, try combining them to improve performance and responsiveness.

Custom promises with Deferred

Мы
реализовали кастомыFuture и
ListenableFutureраньше. Многие разработчики недоумевают, в чем разница между обещаниями и
$.Deferred— именно тогда, когда нам это нужно, — реализовывать пользовательские методы, возвращающие обещания, как
$.ajax()и друзья. Помимо AJAX,
setTimeout()и
setInterval()известны тем, что вводят вложенные обратные вызовы. Можем ли мы сделать лучше с обещаниями? Конечно!

function timeoutPromise(millis, context) {
    var deferred = $.Deferred();
    setTimeout(function() {
        deferred.resolve(context);
    }, millis);
    return deferred.promise();
}
 
var promise = timeoutPromise(1000, 'Boom!');
promise.done(function(s) {
    console.info(s);
});

Каждая строка
timeoutPromise()важна, поэтому внимательно изучите ее. Сначала мы создаем
$.Deferred()экземпляр, который в основном является контейнером для еще не разрешенного значения (будущее). Позже мы регистрируем тайм-аут, чтобы он запускался через
millisмиллисекунды. По истечении этого времени мы
разрешаем отложенный объект. Когда обещание разрешено, doneавтоматически регистрируются все зарегистрированные
обратные вызовы. Наконец мы возвращаем внутренний
promiseобъект клиенту. Ниже вы увидели, как такое обещание может быть использовано — это практически то же самое, что и с AJAX. Можете ли вы угадать, что будет напечатано? Конечно, объект, представленный
contextв
deferred.resolve(context)вызове, это
'Boom!'строка.

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

Мониторинг прогресса

Обещания хороши, но они не подходят, когда мы хотели бы использовать
setInterval()вместо
setTimeout(). Будущее может быть разрешено только один раз, в то время как setInterval()может вызвать
поставленный обратный вызов несколько раз. Но у обещаний jQuery есть одна уникальная особенность, которую мы еще не видели в нашей серии: API мониторинга прогресса. Прежде чем выполнить обещание, мы можем уведомить клиентов о его выполнении. Это имеет смысл для длительных, многоступенчатых процессов. Вот утилита для
setInterval():

function intervalPromise(millis, count) {
    var deferred = $.Deferred();
    if(count <= 0) {
        deferred.reject("Negative repeat count " + count);
    }
    var iteration = 0;
    var id = setInterval(function() {
        deferred.notify(++iteration, count);
        if(iteration >= count) {
            clearInterval(id);
            deferred.resolve();
        }
    }, millis);
    return deferred.promise();
}

intervalPromise()повторяется
countраз в
millisмиллисекунды. Первое уведомление о вызове, по
deferred.reject()которому обещание не будет выполнено немедленно (см. Ниже). Во-вторых, обратите внимание на то,
deferred.notify()что на каждой итерации вызывается уведомление о прогрессе. Вот два эквивалентных способа использования этой функции.
fail()обратный вызов будет использоваться, если обещание было отклонено:

var notifyingPromise = intervalPromise(500, 4);
 
notifyingPromise.
    progress(function(iteration, total) {
        console.debug("Completed ", iteration, "of", total);
    }).
    done(function() {
        console.info("Done");
    }).
    fail(function(e) {
        console.warn(e);
    });

Или же:

intervalPromise(500, 4).then(
    function() {
        console.info("Done");
    },
    function(e) {
        console.warn(e);
    },
    function(iteration, total) {
        console.debug("Completed ", iteration, "of", total);
    }
);

Второй пример, приведенный выше, немного более компактен, но и немного менее читабелен. Но оба они выдают один и тот же результат (сообщения о прогрессе печатаются каждые 500 мс):

Completed 1 of 4
Completed 2 of 4
Completed 3 of 4
Completed 4 of 4
Done

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

function doubleAjax() {
    var deferred = $.Deferred();
    $.getJSON('square/3', function(threeSquare) {
        deferred.notify(threeSquare)
        $.getJSON('square/4', function(fiveSquare) {
            deferred.resolve(fiveSquare);
        });
    });
    return deferred.promise();
}
 
doubleAjax().
    progress(function(threeSquare) {
        console.info("In the middle", threeSquare);
    }).
    done(function(fiveSquare) {
        console.info("Done", fiveSquare);
    });

Обратите внимание, как мы уведомляем, как
promiseтолько первый запрос завершается, и разрешаем его в конце. Клиент может обрабатывать только
done()обратный вызов или оба. С традиционными API на основе обратного вызова мы получили бы
doubleAjax(doneCallback, progressCallback)функцию, принимающую в качестве аргумента две функции, где вторая является необязательной (?)

API-интерфейс Progress недоступен на других основных языках, которые мы исследовали до сих пор, что делает обещания jQuery весьма полезными и интересными.

Цепные и трансформирующие обещания

Еще одна вещь, которой я хотел бы поделиться с вами — это цепочка и преобразование обещаний. Концепция не нова для нас (как в
Java, так и в
Scala / Akka ). Как бы это выглядело в JavaScript? Сначала определите несколько низкоуровневых методов:

function square(value) {
    return $.getJSON('square/' + value);
}
 
function remoteDouble(value) {
    return $.getJSON('double/' + value);
}
 
function localDouble(x) {
    return x * 2;
}

Теперь мы можем легко объединить их:

square(2).then(localDouble).then(function(doubledSquare) {
    console.info(doubledSquare);
});
 
square(2).then(remoteDouble).then(localDouble).then(function(doubledSquare) {
    console.info(doubledSquare);
});

Первый пример применяет
localDouble()функцию, когда результат прибывает (2 квадрата) и умножает его на два. Таким образом , конечный обратный вызов печатает
8. Второй пример гораздо интереснее. Пожалуйста, посмотрите внимательно. Когда
square(2)обещание разрешено, вызовы jQuery
remoteDouble(4)(
4являются результатом асинхронного
square/2вызова AJAX). Но эта функция, опять же, возвращает обещание. После
remoteDouble(4)разрешения (возврат 8) localDouble(8)применяется окончательный
обратный вызов и немедленно возвращается печать
16. Эта конструкция позволяет нам связывать вызовы AJAX (и любые другие обещания), предоставляя результат одного вызова в качестве аргумента для последующего вызова.

Обещания в AngularJS

AngularJS имеет одну действительно удобную функцию, использующую преимущества динамической типизации и обещаний. Я считаю, что jQuery мог бы многому научиться из этой простой идеи и реализовать ее в основной библиотеке. Но вернемся к делу. Это типичный графический интерфейс обновления взаимодействия AJAX в AngularJS:

angular.module('promise', []);
 
function Controller($scope, $http) {
    $http.get('square/3').success(function(reply) {
        $scope.result = {data: reply};
    });
}

Где шаблон выглядит следующим образом:

<body ng-app="promise" ng-controller="Controller">
    3 square: {{result.data}}
</body>

Если вы не знакомы с AngularJS — присваивание значения для
$scopeавтоматического обновления всех элементов DOM, относящихся к измененным переменным области видимости. Таким образом, запуск этого приложения будет выполняться
3 square: 9после получения ответа. Выглядит довольно чисто (обратите внимание, что AngularJS также использует обещания!) Но мы можем сделать намного лучше! Сначала немного кода:

function Controller($scope, $http) {
    $scope.result = $http.get('square/3');
}

Этот код гораздо умнее, чем кажется. Помните, что
$http.get()возвращает
обещание , а не ценность. Это означает, что мы назначаем обещание (возможно, еще не получил ответ AJAX) нашей области. Все еще не понимаю, почему я так взволнован? Пытаться:

`$('.result').text($.getJSON('square/3'))`

в jQuery.
Не будет работать . Но AngularJS достаточно умен, чтобы признать, что переменная области действия — это обещание. Таким образом, вместо того, чтобы пытаться отрендерить его (приводит к
[object Object]), он просто ждет его разрешения. Как только обещание выполнено, оно заменяет его значением и обновляет DOM. Автоматически. Не нужно использовать обратные вызовы, фреймворк поймет, что мы не хотим отображать обещание, а его значение, когда оно будет разрешено И, кстати, AngularJS имеет свою реализацию
Deferredи обещает в
$qобслуживании .

Резюме

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