Статьи

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

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
$.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 :

1
2
3
4
5
6
7
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 для регистрации обратных вызовов:

1
2
3
4
5
6
7
8
9
promise3.done(function(threeSquare) {
    console.info(threeSquare);
});
promise3.done(function() {
    console.debug("Done");
});
promise3.done(function(threeSquare) {
    $('.result').text(threeSquare);
});

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

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

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

1
2
3
4
5
6
7
8
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(promise3, promise5) и преобразовать его. Недостатком $.when является то, что он не принимает (распознает) массив обещаний. Но JavaScript достаточно динамичен, чтобы легко его обойти:

01
02
03
04
05
06
07
08
09
10
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. Каждое разрешенное обещание передается как отдельный аргумент, поэтому мы должны использовать псевдомассив arguments чтобы захватить их все.
  3. Обратный вызов done() выполняется, когда все обещания разрешены (возвращается последний вызов AJAX), но обещания могут поступать из любого источника, необязательно из запроса AJAX (подробнее о Deferred объекте читайте далее)
  4. $.when() имеет точно Futures.allAsList() же семантику, что и Futures.allAsList() в Guava и Future.sequence() в Akka / Scala .
  5. (sidenote) Инициирование нескольких вызовов AJAX одновременно не обязательно является лучшим дизайном, попробуйте объединить их для повышения производительности и скорости отклика.

Пользовательские обещания с Deferred

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

01
02
03
04
05
06
07
08
09
10
11
12
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() каждая строка timeoutPromise() поэтому внимательно изучите ее. Сначала мы создаем $.Deferred() который в основном является контейнером для еще не разрешенного значения (future). Позже мы регистрируем тайм-аут, чтобы он millis миллисекунды. По истечении этого времени мы разрешаем отложенный объект. Когда обещание разрешено, автоматически регистрируются все зарегистрированные done обратные вызовы. Наконец мы возвращаем объект внутреннего promise клиенту. Ниже вы увидели, как такое обещание может быть использовано — это практически то же самое, что и с AJAX. Можете ли вы угадать, что будет напечатано? Конечно, объект, представленный context в вызове deferred.resolve(context) , то есть 'Boom!' строка.

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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 раз каждые миллисекунды. Сначала обратите внимание на вызов deferred.reject() который сразу же не выполнит обещание (см. Ниже). Во-вторых, обратите внимание на deferred.notify() который вызывается на каждой итерации для уведомления о прогрессе. Вот два эквивалентных способа использования этой функции. Обратный вызов fail() будет использоваться, если обещание было отклонено:

01
02
03
04
05
06
07
08
09
10
11
12
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);
    });

Или же:

01
02
03
04
05
06
07
08
09
10
11
intervalPromise(500, 4).then(
    function() {
        console.info("Done");
    },
    function(e) {
        console.warn(e);
    },
    function(iteration, total) {
        console.debug("Completed ", iteration, "of", total);
    }
);

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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 когда первый запрос завершен, и разрешаем его в конце. Клиент может обрабатывать только callback done() или оба. В случае традиционных API на основе обратного вызова мы получили бы doubleAjax(doneCallback, progressCallback) принимающую в качестве аргумента две функции, в то время как вторая является необязательной (?). довольно полезно и интересно.

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

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

01
02
03
04
05
06
07
08
09
10
11
function square(value) {
    return $.getJSON('square/' + value);
}
  
function remoteDouble(value) {
    return $.getJSON('double/' + value);
}
  
function localDouble(x) {
    return x * 2;
}

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

1
2
3
4
5
6
7
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) обещание разрешено, мы вызываем remoteDouble(4) ( 4 является результатом асинхронного square/2 AJAX-вызова). Но эта функция, опять же, возвращает обещание. Окончательный обратный вызов печатает 8 (результат double/4 вызова), когда возвращается это второе обещание. Эта конструкция позволяет нам связывать вызовы AJAX (и любые другие обещания), предоставляя результат одного вызова в качестве аргумента для последующего вызова.

Обещания в AngularJS

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

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

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

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

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

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

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

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

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

Резюме

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

Ссылка: Обещания и отложенные объекты в jQuery и AngularJS от нашего партнера по JCG Томаша Нуркевича в блоге NoBlogDefFound .