Статьи

JavaScript обещает быть классным

«И когда я что-то обещаю, я никогда не нарушаю это обещание. Никогда.» Рапунцель

Во многих языках есть библиотеки интересных схем, называемых обещаниями, отсрочками или фьючерсами. Они помогают укротить дикую асинхронность во что-то, похожее на мирскую последовательность. Обещания JavaScript могут способствовать разделению интересов вместо тесно связанных интерфейсов. Эта статья о обещаниях JavaScript из серии Обещания / A. [ http://wiki.commonjs.org/wiki/Promises/A ]

Обещание вариантов использования:

  • Выполнение правил
  • Несколько удаленных проверок
  • Обработка таймаутов
  • Удаленные запросы данных
  • Анимация
  • Отделение логики событий от логики приложения
  • Устранение обратного вызова треугольника гибели
  • Управление параллельными асинхронными операциями

Обещание JavaScript — это долговая расписка для возврата значения в будущем. Это объект данных, имеющий четко определенное поведение. Обещание имеет одно из трех возможных состояний:

  1. В ожидании
  2. Отклонено
  3. Постановили

Отклоненное или разрешенное обещание считается исчерпанным . Состояние обещания может только перейти от ожидания к урегулированию. После этого его состояние является неизменным. Обещание может быть выполнено в течение долгого времени после того, как соответствующие действия урегулированы В свободное время мы можем извлечь результат несколько раз. Мы осуществляем это путем вызова обещания .then () . Этот вызов не будет возвращен до тех пор, пока не будет выполнено соответствующее действие. Мы можем плавно цеплять обещания. Каждая из связанных функций «тогда» должна либо возвращать обещание, либо позволить исходному обещанию быть возвращаемым значением.

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

  • Сложенные задачи: несколько моментов разбросаны в коде, но с одинаковым обещанием.
  • Параллельные задачи: несколько обещаний возвращают одно обещание.
  • Последовательные задачи: обещай… потом… обещай
  • Сочетания этих.

Почему этот дополнительный слой? Почему мы не можем просто использовать необработанные обратные вызовы?

Проблемы с обратными вызовами

Обратные вызовы хороши для реакции на простые повторяющиеся события, такие как включение значения формы на основе щелчка или для сохранения результата вызова REST. Обратные вызовы также побуждают человека кодировать в цепочке, когда один обратный вызов выполняет следующий вызов REST, в свою очередь предоставляя обратный вызов, который выполняет следующий вызов REST, и так далее. Это имеет тенденцию к пирамиде гибели, показанной на рисунке 1. Там код растет горизонтально быстрее, чем вертикально. Обратные вызовы кажутся простыми … пока нам не нужен результат, который мы могли бы использовать на следующем шаге нашего кода.

Рисунок 1: Анти-паттерн Пирамида Судьбы + код ниже

Рисунок 1: Анти-паттерн Пирамида Судьбы + код ниже

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
'use strict';
var i = 0;
function log(data) {console.log('%d %s', ++i, data); };
 
function validate() {
   log("Wait for it ...");
   // Sequence of four Long-running async activities
   setTimeout(function () {
      log('result first');
      setTimeout(function () {
         log('result second');
         setTimeout(function () {
            log('result third');
            setTimeout(function () {
               log('result fourth')
            }, 1000);
         }, 1000);
      }, 1000);
   }, 1000);
 
};
validate();

На рисунке 1 я использовал тайм-ауты для имитации асинхронных действий. Понятие об управлении исключениями, которые могли бы управляться с последующими действиями, болезненно. Когда нам приходится составлять обратные вызовы, организация кода становится грязной. На рисунке 2 показан фиктивный поток проверки, который будет запущен при вставке в NodeJS REPL. В следующем разделе мы перенесем его из паттерна пирамиды судьбы в последовательное исполнение обещаний.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'use strict';
var i = 0;
function log(data) {console.log('%d %s', ++i, data); };
 
// Asynchronous fn executes a callback result fn
function async(arg, callBack) {
   setTimeout(function(){
      log('result ' + arg);
      callBack();
   }, 1000);
};
 
function validate() {
   log("Wait for it ...");
   // Sequence of four Long-running async activities
   async('first', function () {
      async('second',function () {
         async('third', function () {
            async('fourth', function () {});
         });
      });
   });
};
validate();

Выполнение в NodeJS REPL дает:

1
2
3
4
5
6
7
$ node scripts/examp2b.js
1 Wait for it ...
2 result first
3 result second
4 result third
5 result fourth
$

Однажды у меня была ситуация динамической проверки в AngularJS, где значения форм могли быть динамически обязательными, в зависимости от значений форм равноправных узлов. Служба REST определяла действительное значение каждого обязательного элемента. Я избежал вложенных обратных вызовов, написав диспетчер, который работал со стеком функций, в зависимости от того, какие значения требовались. Диспетчер извлекает функцию из стека и выполняет ее. Обратный вызов этой функции завершится вызовом моего диспетчера, чтобы повторить, пока стек не опустошится. Каждый обратный вызов записывал любую ошибку проверки, возвращенную его удаленным вызовом проверки.

Я считаю, что мое противозачаточное средство является антипаттерном Если бы я использовал альтернативу обещания, предложенную Angular $ http call, мое мышление для всей проверки было бы похоже на линейную форму, напоминающую синхронное программирование. Сглаженная цепочка обещаний читаема. Читать дальше …

Использование обещаний

На рисунке 3 показана моя искусственная проверка достоверности в цепочке обещаний. Он использует библиотеку обещаний кью. Библиотека Q работает одинаково хорошо. Чтобы попробовать это, сначала используйте npm для импорта библиотеки kew в NodeJS, а затем загрузите код в REPL NodeJS.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
'use strict';
var Q = require('kew');
var i = 0;
 
function log(data) {console.log('%d %s', ++i, data); };
 
// Asynchronous fn returns a promise
function async(arg) {
    var deferred = Q.defer();
    setTimeout(function () {
        deferred.resolve('result ' + arg);\
    }, 1000);
    return deferred.promise;
};
 
// Flattened promise chain
function validate() {
    log("Wait for it ...");
    async('first').then(function(resp){
        log(resp);
        return async('second');
    })
    .then(function(resp){
        log(resp);
        return async('third')
    })
    .then(function(resp){
        log(resp);
        return async('fourth');
    })
    .then(function(resp){
        log(resp);
    }).fail(log);
};
validate();

Вывод такой же, как и у вложенных обратных вызовов:

1
2
3
4
5
6
7
$ node scripts/examp2-pflat.js
1 Wait for it ...
2 result first
3 result second
4 result third
5 result fourth
$

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

Сервер или браузер

Обещания полезны как в браузере, так и на сервере NodeJS. Следующий URL, http://jsfiddle.net/mauget/DnQDx/ , указывает на JSFiddle, который показывает, как использовать одно обещание на веб-странице. Весь код является изменяемым в JSFiddle. Один из вариантов вывода браузера представлен на рисунке 4. Я сфальсифицировал действие, чтобы случайно отклонить его. Вы можете попробовать это несколько раз, чтобы получить противоположный результат. Было бы просто расширить его до цепочки многообещающих обещаний, как в предыдущем примере NodeJS.

Рисунок 4: Одно обещание

Рисунок 4: Одно обещание

Параллельные обещания

Рассмотрим асинхронную операцию, передающую другое асинхронное действие. Пусть последний состоит из трех параллельных асинхронных действий, которые, в свою очередь, обеспечивают окончательное действие. Он устанавливается только тогда, когда удовлетворяются все параллельные дочерние запросы. См. Рисунок 5. Это связано с благоприятной встречей с цепочкой из двенадцати операций MongoDB. Некоторые имели право работать параллельно. Я реализовал поток с обещаниями.

Рисунок 5: Состав асинхронных действий

Рисунок 5: Состав асинхронных действий

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
var promiseVals = ['To ', 'be, ', 'or ',
        'not ', 'to ', 'be, ', 'that ',
        'is ', 'the ', 'question.'];
 
    var startParallelActions = function (){
        var promises = [];
 
        // Make an asynchronous action from each literal
        promiseVals.forEach(function(value){
            promises.push(makeAPromise(value));
        });
 
        // Consolidate all promises into a promise of promises
        return Q.all(promises);
    };
 
    startParallelActions ().then( . . .

Следующий URL, http://jsfiddle.net/mauget/XKCy2/ , нацелен на JSFiddle, который выполняет 10 параллельных обещаний в браузере, отклоняя или решая произвольно. Полный код доступен для проверки и изменений. Повторно запускайте, пока не получите противоположное завершение. На рисунке 7 показан положительный результат.

Рисунок 7: демонстрация параллельных обещаний JSFiddle

Рисунок 7: демонстрация параллельных обещаний JSFiddle

Рождение Обещание

Многие API возвращают обещание, имеющее функцию then — они осуществимы . Обычно я мог бы просто связать a затем к результату функции. В противном случае библиотеки $ q, mpromise, Q и kew имеют простой API, используемый для создания, отклонения или разрешения обещания. В разделе ссылок есть ссылки на документацию по API для каждой библиотеки. Мне обычно не нужно было создавать обещание, за исключением того, что я обернул невежественные литералы обещаний и функции тайм-аута для этой статьи. Смотрите примеры, где я создал обещания.

Взаимодействие библиотеки обещаний

Большинство библиотек обещаний JavaScript взаимодействуют на том уровне. Вы можете создать обещание из иностранного обещания, потому что обещание может обернуть любую ценность. Это работает через библиотеки, которые поддерживают тогда . Помимо этого существуют разные функции обещания. Если вам нужна функция, которой нет в вашей библиотеке, вы можете заключить обещание из вашей библиотеки в новое обещание из библиотеки, в которой есть нужная вам функция. Например, обещания JQuery иногда порочатся в литературе. Вы можете сразу обернуть их в обещание Q, $ q, mpromise или kew для работы в этой библиотеке.

в заключение

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

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

Если вы не использовали обещания напрямую, попробуйте их. Решено: у вас будет хороший опыт. Я обещаю!

Ссылка: Обещания JavaScript хороши от нашего партнера JCG Лу Може в блоге