Все об обещаниях (для приложений Магазина Windows, написанных на JavaScript)
При написании приложений Магазина Windows на языке JavaScript, как только вы захотите создать что-либо, связанное с асинхронным API, вы столкнетесь с конструкциями, называемыми обещаниями (объектами отложенного результата). Пройдет немного времени, и написание цепочек обещаний для последовательных асинхронных операций станет для вас привычным делом.
При разработке приложений вам наверняка повстречаются и другие, не совсем очевидные способы использования обещаний. Хорошим примером этого является оптимизация функций отрисовки элементов для элемента управления ListView, представленная в примере оптимизации производительности HTML ListView. Мы рассмотрим эту тему подробнее в следующей статье. Или ознакомьтесь с крайне интересными сведениями, представленными Джошем Вильямсом (Josh Williams) в выступлении Подробный обзор WinJS на конференции //build 2012 (материал немного изменен):
list.reduce(function callback (prev, item, i) { var result = doOperationAsync(item); return WinJS.Promise.join({ prev: prev, result: result}).then(function (v) { console.log(i + ", item: " + item+ ", " + v.result); }); })
Этот фрагмент кода объединяет обещания для параллельных асинхронных операций и выдает результаты последовательно в соответствии с их порядком в списке. Если, глядя на этот код, вы можете сразу же описать его назначение, можете пропустить данную статью. Если нет, давайте подробнее рассмотрим работу обещаний и их выражение в WinJS — библиотеке Windows для JavaScript, чтобы понимать схему их действия.
Что собой представляет обещание? Отношения в случае обещания
Давайте начнем с основополагающего факта: обещание — это просто конструкция кода или, если желаете, соглашение о вызовах. Таким образом, обещания не имеют обязательной связи с асинхронными операциями — просто их крайне удобно использовать в этом отношении. Обещание — это просто объект, представляющий значение, которое может быть доступно в некоторый момент в будущем либо прямо сейчас. Получается, что сущность обещания соответствует смыслу данного термина, когда он употребляется для описания отношений между людьми. Если я говорю: "Я обещаю принести вам десять бубликов", — это не значит, что эти бублики есть у меня прямо сейчас, однако я определенно подразумеваю, что я получу их в некоторый момент в будущем. И когда это произойдет, я их принесу.
Таким образом, обещание подразумевает отношение между двумя агентами: инициатором, который обещает предоставление некоторых товаров, и потребителем, который является получателем как такого обещания, так и непосредственно товаров. Как именно инициатор собирается получить товары, это его личное дело. Аналогичным образом, потребитель может делать все, что угодно, как с обещанием, так и с доставленными товарами. Он даже может использовать обещание совместно с другими потребителями.
Между инициатором и потребителем лежит два этапа этого отношения — создание и исполнение. Все это приведено на следующей схеме.
Именно благодаря наличию двух этапов этого отношения, обещания отлично подходят для асинхронной доставки, как видно из приведенного на схеме потока. Основная идея заключается в том, что после получения подтверждения запроса, то есть обещания, потребитель может продолжать работу (асинхронный режим) вместо простого ожидания (синхронный режим). Это значит, что во время ожидания исполнения обещания потребитель может выполнять другие операции, например отвечать на другие запросы, в чем и заключается основная задача асинхронных API. А что если товары уже доступны? Тогда обещание исполняется моментально, как в случае соглашения о синхронных вызовах.
Конечно, это отношение имеет и другие особенности, которые нам следует учитывать. В своей жизни вы наверняка давали и принимали обещания. Хотя многие из них были выполнены, в реальности многие обещания нарушаются, например по пути к вам домой курьер из магазина может попасть аварию. Нарушенные обещания являются неотъемлемой частью жизни, поэтому нам следует принять это как должное и в личной жизни, и в асинхронном программировании.
С точки зрения отношений с использованием обещаний это значит, что инициатор должен иметь возможность сказать: "К сожалению, я не могу доставить товары по этому обещанию", — а потребитель должен иметь возможность узнать об этом. Во-вторых, когда мы выступаем в роли потребителя, то можем быть весьма нетерпеливыми, дожидаясь выполнения полученных обещаний. Поэтому, если инициатор может отслеживать ход выполнения своего обещания, потребителю также нужен способ для получения такой информации. В-третьих, потребитель также может отменить заказ и сообщить инициатору, что товары ему больше не требуются.
Добавив эти требования в схему, мы получаем полноценное отношение:
Теперь давайте рассмотрим, как эти отношения проявляются в коде.
Конструкция обещания и цепочки обещаний
Для обещаний доступно множество разных предложений или спецификаций. Одним из них, используемым в Windows и WinJS, является Common JS/Promises A, где обещание описывается как нечто, возвращаемое инициатором для представления значения, предоставляемого в будущем, и является объектом с функцией then. Потребители подписываются на выполнение обещания, вызывая метод then. (В Windows обещания также поддерживают аналогичную функцию done, используемую, как мы скоро увидим, в цепочках обещаний.)
В эту функцию в качестве аргументов потребитель передает до трех дополнительных функций в следующем порядке:
- Обработчик выполнения. Инициатор вызывает эту функцию, когда обещанное значение доступно, и если оно доступно прямо сейчас, обработчик выполнения сразу (синхронно) вызывается из метода then.
- Дополнительный обработчик ошибок, вызываемый в случае сбоя при получении обещанного значения. Если для любого отдельного обещания был вызван обработчик ошибок, обработчик выполнения для него не вызывается никогда.
- Дополнительный обработчик хода выполнения, который периодически вызывается с промежуточными результатами, если это поддерживает выполняемая операция. (В WinRT это означает, что API имеет возвращаемое значение IAsync[Action | Operation]WithProgress, а интерфейсы с IAsync[Action | Operation] — нет.)
Обратите внимание на то, что для любого из этих аргументов можно передать значение null, например, если вы хотите назначить только обработчик ошибок без обработчика выполнения.
Потребитель, находящийся на другой стороне отношения, может подписаться на любое число обработчиков для одного обещания, вызывая then несколько раз. Он также может использовать обещание совместно с другими потребителями, которые также имеют возможность вызывать метод then любое число раз. Все это полностью поддерживается.
Это значит, что обещание должно управлять списками всех получаемых обработчиков и вызывать их в нужное время. Кроме того, обещания должны допускать отмену, как указано на полной схеме отношения.
Другое требование, следующее из спецификации Promises A, заключается в возвращении обещания самим методом then. Такое второе обещание выполняется, когда возвращает данные обработчик выполнения, назначенный первой функции promise.then, и это возвращаемое значение доставляется в качестве результата второго обещания. Рассмотрите следующий фрагмент кода:
var promise1 = someOperationAsync(); var promise2 = promise1.then(function completedHandler1 (result1) { return 7103; } ); promise2.then(function completedHandler2 (result2) { });
Здесь цепочка выполнения начинается с запуска someOperationAsync, возвращающего promise1. Во время выполнения этой операции мы вызываем функцию promise1.then, которая немедленно возвращает promise2. Следует понимать, что completedHandler1 вызывается только в тех случаях, когда результат асинхронной операции уже доступен. Предположим, что мы все еще ждем, поэтому выполняем вызов promise2.then, и снова completedHandler2 при этом не вызывается.
Немного позднее someOperationAsync завершается со значением, например 14618. Обещание promise1 теперь выполнено, поэтому оно вызывает completedHandler1 с этим значением, и результат result1 будет равен 14618. Теперь выполняется completedHandler1, возвращая значение 7103. При этом выполняется обещание promise2, поэтому оно вызывает completedHandler2 с результатом result2, равным 7103.
А что если обработчик выполнения возвращает еще одно обещание? Этот случай обрабатывается немного иначе. Предположим, что completedHandler1 из предыдущего кода возвращает следующее обещание:
var promise2 = promise1.then(function completedHandler1 (result1) { var promise2a = anotherOperationAsync(); return promise2a; });
В этом случае result2 в completedHandler2 будет не самим promise2a, а значением fulfillment для promise2a. Таким образом, поскольку обработчик выполнения возвращает обещание, promise2, как возвращенное из promise1.then, будет выполнено с результатами promise2a.
Именно это делает возможным объединение последовательных асинхронных операций в цепочки, где результаты каждой операции цепочки передаются в следующую операцию. Без промежуточных переменных или именованных обработчиков цепочки обещаний часто имеют следующую структуру:
operation1().then(function (result1) { return operation2(result1) }).then(function (result2) { return operation3(result2); }).then(function (result3) { return operation4(result3); }).then(function (result4) { return operation5(result4) }).then(function (result5) { //And so on });
Конечно, с большой долей вероятности каждый обработчик выполнения выполняет с получаемыми результатами дополнительные действия, однако данная базовая структура присутствует во всех цепочках. Кроме того, здесь все методы then выполняются один за другим, поскольку они только сохраняют заданный обработчик выполнения и возвращают еще одно обещание. Таким образом, когда мы добрались до конца кода, была запущена только operation1 и не были вызваны никакие обработчики выполнения. Однако был создан набор связанных между собой промежуточных обещаний из всех вызовов then, чтобы цепочкой можно было управлять как ходом выполнения последовательных операций.
Следует отметить, что аналогичную последовательность можно получить, вкладывая каждую последующую операцию в предыдущий обработчик выполнения; в этом случае у вас не будет всех операторов return. Однако такие вложения становятся кошмаром при структурировании кода с использованием отступов, особенно если вы начинаете добавлять обработчики хода выполнения и ошибок вместе с каждым вызовом then.
Кстати, одна из возможностей обещаний в WinJS заключается в том, что ошибки, возникающие в любой части цепочки, автоматически распространяются до ее конца. Это означает, что вместо использования обработчиков ошибок на каждом уровне, вы можете просто прикрепить отдельный обработчик ошибок к последнему вызову then. Здесь однако следует предупредить, что по различным причинам такие ошибки поглощаются в случае, если последним звеном цепочки является вызов then. Именно поэтому WinJS предоставляет для обещаний еще и метод done. Он принимает те же аргументы, что и метод then, однако указывает, что цепочка завершена (возвращает undefined вместо очередного обещания). Любой обработчик ошибок, прикрепленный к методу done, вызывается для всех ошибок во всей цепочке. Кроме того, если метод done не имеет обработчик ошибок, он выдает исключение на уровне приложений, где оно может быть обработано window.onerror событий WinJS.Application.onerror. Одним словом, в идеальном случае все цепочки должны заканчиваться методом done, чтобы обеспечить предоставление и правильную обработку исключений.
Конечно, если вы создаете функцию, предназначенную для возврата последнего обещания из длинной цепочки вызовов then, то все равно используете в конце then: в этом случае ответственность за обработку ошибок несет вызывающий объект, который может параллельно использовать это обещание в другой цепочке.
Создание обещаний: класс WinJS.Promise
Хотя вы всегда можете создать собственные классы обещаний на базе спецификации Promises A, это требует значительных усилий, которые можно сэкономить, используя библиотеку. Поэтому WinJS предоставляет надежный, протестированный и гибкий класс обещаний WinJS.Promise. Он позволяет легко создавать обещания для разных значений и операций, не вникая в тонкости управления отношениями инициатора и потребителя или работы функции then.
Когда потребуется, вы можете (и должны) использовать новый класс WinJS.Promise (либо подходящую вспомогательную функцию, как указано в следующем разделе) в целях создания обещаний как для асинхронных операций, так и для существующих (синхронных) значений. Помните о том, что обещание — это просто конструкция кода: нет никакого требования, предписывающего, чтобы обещание охватывало асинхронную операцию или что бы то ни было асинхронное. Аналогичным образом, сам факт заключения фрагмента кода в обещание еще не гарантирует асинхронности его выполнения . Эту работу вам придется сделать самостоятельно.
В качестве простого примера прямого использования класса WinJS.Promise давайте предположим, что требуется выполнить длинное вычисление — простое суммирование набора значений от единицы до некоторого максимума, делая это в асинхронном режиме. Мы могли бы разработать для такой подпрограммы свой механизм обратного вызова, но если мы заключим ее в обещание, мы обеспечим возможность связывания в цепочку или соединения с другими обещаниями из других API. (По этому принципу функция WinJS.xhr заключает асинхронный запрос XmlHttpRequest JavaScript в обещание, чтобы вам не пришлось разбираться в структуре событий такого запроса.)
Разумеется, мы можем использовать для длинного вычисления рабочий процесс JavaScript, но, чтобы проиллюстрировать этот пример, мы продолжим работать в потоке пользовательского интерфейса и воспользуемся setImmediate для разделения этой операции на этапы. Вот как это можно реализовать в структуре обещаний, используя WinJS.Promise:
function calculateIntegerSum(max, step) { //The WinJS.Promise constructor's argument is an initializer function that receives //dispatchers for completed, error, and progress cases. return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) { var sum = 0; function iterate(args) { for (var i = args.start; i < args.end; i++) { sum += i; }; if (i >= max) { //Complete--dispatch results to completed handlers completeDispatch(sum); } else { //Dispatch intermediate results to progress handlers progressDispatch(sum); setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) }); } } setImmediate(iterate, { start: 0, end: Math.min(step, max) }); }); }
При вызове нового класса WinJS.Promise единственным аргументом для его конструктора является функция initializer (в данном случае анонимная). Функция initializer инкапсулирует выполняемую операцию, однако очевидно, что сама эта функция в потоке пользовательского интерфейса выполняется синхронно. Если бы сейчас мы выполнили длинное вычисление, не используя setImmediate, то заблокировали бы поток пользовательского интерфейса на все время вычисления. Еще раз отметим — помещение кода внутрь обещания еще не делает этот кодасинхронным, это необходимо настроить в функции initializer.
В качестве аргументов функция initializer принимает три диспетчера для случаев выполнения, ошибки и хода выполнения, которые поддерживаются этими обещаниями. Как видите, мы вызываем эти диспетчеры в нужные моменты во время операции с использованием подходящих аргументов.
Я называю эти функции "диспетчерами", поскольку они отличаются от обработчиков, которые потребители подписывают на метод then обещания (или метод done, но я больше не буду напоминать об этом). Если разобраться, WinJS управляет массивами этих обработчиков, что позволяет любому числу потребителей подписаться на любое число обработчиков. Когда вы вызываете один из таких диспетчеров, WinJS выполняет итерации по своему внутреннему списку и вызывает все эти обработчики от вашего имени. Кроме того, WinJS.Promise проверяет, что метод then возвращает другое обещание, что необходимо для образования цепочки.
По сути, WinJS.Promise предоставляет все детали, окружающие и дополняющие обещание. Благодаря этому вы можете сконцентрироваться на основной операции обещания, которая помещается в функцию initializer.
Вспомогательные средства для создания обещаний
Основной вспомогательной функцией для создания обещания является статический метод WinJS.Promise.as, охватывающий любое значение в обещании Применение такой оболочки для уже существующего значения не привносит ничего нового и вызывает любой обработчик выполнения, переданный в then. Это позволяет обрабатывать произвольные известные значения как обещания, чтобы вы могли смешивать и сопрягать их с другими обещаниями (посредством присоединения или объединения в цепочку). Использование as для существующего обещания просто возвращает само это обещание.
Другой статической вспомогательной функцией является WinJS.Promise.timeout, которая предоставляет удобную оболочку для setTimeout и setImmediate. Вы также можете создать обещание, которое отменяет второе обещание, если это второе обещание не выполнено в течение заданного числа миллисекунд.
Обратите внимание, что обещания timeout для setTimeout и setImmediate сами исполняются с undefined. Часто спрашивают: "Как можно использовать их для предоставления других результатов после истечения времени ожидания?" В ответе используется тот факт, что функция then возвращает другое обещание, выполняемое с возвращаемым значением обработчика выполнения. Приведем в качестве примера следующую строку кода:
var p = WinJS.Promise.timeout(1000).then(function () { return 12345; });
Она создает обещание p, которое будет выполнено со значением 12345 через одну секунду. Другими словами, WinJS.Promise.timeout(…).then(function () { return <значение>} ) является шаблоном для предоставления <значения> после истечения времени ожидания. А если само <значение> является другим обещанием, оно служит для предоставления значения выполнения из этого обещания в определенный момент после истечения времени ожидания.
Ошибки при отмене и создании обещаний
В предыдущем коде вы могли заметить два недостатка. Первый заключается в отсутствии способа отмены операции после ее начала. Второй — в неудовлетворительной обработке ошибок.
В обоих случаях особенность состоит в том, что функции создания обещаний, такие как calculateIntegerSum, должны всегда возвращать обещание. Если операция не может быть выполнена или никогда не запускается, такое обещание находится в состоянии ошибки. Это значит, что обещание не имеет и никогда не будет иметь результат, который оно сможет передать в любой из обработчиков выполнения: оно просто постоянно вызывает свои обработчики ошибок. В самом деле, если потребитель вызывает then для обещания, которое уже находится в состоянии ошибки, это обещание немедленно (синхронно) вызывает обработчик ошибок, назначенный then.
WinJS.Promise переходит в состояние ошибки по двум причинам: потребитель вызывает свой метод cancel, либо код в функции initializer вызывает диспетчер ошибок. Когда такое происходит, обработчики ошибок получают значение ошибки, которое было перехвачено или распространено в обещании. Если вы создаете операцию в WinJS.Promise, то можете также использовать экземпляр WinJS.ErrorFromName. Это просто объект JavaScript, содержащий свойство name, идентифицирующее ошибку, и свойство message, содержащее дополнительную информацию. Например, при отмене обещания обработчики ошибок получают объект ошибки, в котором как для name, так и для message установлено значение "Canceled".
Но что если вы не можете даже запустить операцию? Например, если вы вызываете функцию calculateIntegerSum с неправильными аргументами (например, 0, 0), она даже не должна пытаться начать считать, а должна возвратить обещание в состоянии ошибки. Именно для этого и служит статический метод WinJS.Promise.wrapError. Он принимает экземпляр WinJS.ErrorFromName и возвращает обещание в состоянии ошибки, которое нам требуется в данном случае вместо нового экземпляра WinJS.Promise.
Другая особенность заключается в том, что хотя вызов метода cancel обещания переводит само обещание в состояние ошибки, возникает вопрос: как остановить асинхронную операцию, которая уже выполняется? В предыдущей реализации calculateIntegerSum она просто продолжала вызывать setImmediate до выполнения операции, независимо от состояния созданного нами обещания. Фактически, если операция вызывает диспетчер выполнения после отмены обещания, это обещание просто игнорирует такое завершение.
Поэтому необходим способ, с помощью которого обещание сможет сообщить операции, что ее выполнение больше не требуется. Для этого конструктор WinJS.Promise использует второй аргумент функции, вызываемый при отмене обещания. В нашем примере при вызове этой функции потребовалось бы предотвратить следующий вызов setImmediate, что привело бы к остановке вычисления. Вот как это выглядит с правильной обработкой ошибок:
function calculateIntegerSum(max, step) { //Return a promise in the error state for bad arguments if (max < 1 || step < 1) { var err = new WinJS.ErrorFromName("calculateIntegerSum", "max and step must be 1 or greater"); return WinJS.Promise.wrapError(err); } var _cancel = false; //The WinJS.Promise constructor's argument is an initializer function that receives //dispatchers for completed, error, and progress cases. return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) { var sum = 0; function iterate(args) { for (var i = args.start; i < args.end; i++) { sum += i; }; //If for some reason there was an error, create the error with WinJS.ErrorFromName //and pass to errorDispatch if (false /* replace with any necessary error check -- we don’t have any here */) { errorDispatch(new WinJS.ErrorFromName("calculateIntegerSum (scenario 7)", "error occurred")); } if (i >= max) { //Complete--dispatch results to completed handlers completeDispatch(sum); } else { //Dispatch intermediate results to progress handlers progressDispatch(sum); //Interrupt the operation if canceled if (!_cancel) { setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) }); } } } setImmediate(iterate, { start: 0, end: Math.min(step, max) }); }, //Cancellation function for the WinJS.Promise constructor function () { _cancel = true; }); }
В целом создание экземпляров WinJS.Promise имеет множество применений. Например, если у вас есть библиотека, взаимодействующая с веб-службой посредством другого асинхронного метода, вы можете заключить эти операции в обещания. Вы также можете использовать новое обещание для объединения нескольких асинхронных операций (или других обещаний) из разных источников в одно обещание в тех случаях, когда требуется управлять всеми доступными отношениями. В коде функции initializer для WinJS.Promise вы, несомненно, можете использовать собственные обработчики для остальных асинхронных операций и их обещаний. Их можно использовать, чтобы инкапсулировать механизмы автоматического выполнения повторных попыток для времени недоступности сети и т. п., подключения к универсальному пользовательскому интерфейсу обновления хода выполнения или добавления встроенных функций ведения журналов или аналитики. Во всех этих случаях не нужно сообщать отдельные подробности в остальной код, и он может просто работать с обещаниями со стороны потребителей.
Таким образом довольно просто заключить рабочий процесс JavaScript в обещание, чтобы он выглядел и работал как другие асинхронные операции в WinRT. Возможно, вы уже знаете, что рабочие процессы предоставляют свои результаты посредством вызова postMessage, вызывающего событие message для объекта рабочего процесса в приложении. Следующий код связывает это событие с обещанием, которое выполняется при доставке любых результатов в этом сообщении:
// This is the function variable we're wiring up. var workerCompleteDispatch = null; var promiseJS = new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) { workerCompleteDispatch = completeDispatch; }); // Worker is created here and stored in the 'worker' variable // Listen for worker events worker.onmessage = function (e) { if (workerCompleteDispatch != null) { workerCompleteDispatch(e.data.results); /* event args depends on the worker */ } } promiseJS.done(function (result) { // Output for JS worker });
Чтобы дополнить этот код для обработки ошибок из рабочего процесса, сохраните диспетчер ошибок в другой переменной, сделайте так, чтобы обработчик событий message проверил наличие информации об ошибке в своих аргументах событий, а также при необходимости вызовите диспетчер ошибок вместо диспетчера выполнения.
Объединение параллельных обещаний
Поскольку обещания часто используются для представления асинхронных операций, вы определенно можете обеспечить параллельное выполнение нескольких операций. В этих случаях вам может потребоваться узнать, когда выполняется одно из обещаний в группе либо выполняются все обещания в группе. Для этого служат статические функции WinJS.Promise.any и WinJS.Promise.join.
Обе функции принимают массив значений или объект со свойствами значений. Эти значения могут быть обещаниями, а все отличные от обещаний значения описываются с помощью WinJS.Promise.as, в результате весь массив или объект состоит из обещаний.
Ниже представлены характеристики функции any:
- Функция any создает отдельное обещание, которое выполняется, когда выполняется или завершается с ошибкой одно из других обещаний (логическое ИЛИ). Фактически, функция anyподключает обработчики выполнения ко всем этим обещаниям, и как только вызывается один из обработчиков выполнения, она вызывает все обработчики выполнения, принятые обещанием any.
- После выполнения обещания any(то есть после выполнения первого обещания в списке) остальные операции в списке продолжают выполняться, вызывая все обработчики выполнения, ошибок или хода выполнения, назначенные таким обещаниям в индивидуальном порядке.
- Если вы отменяете обещание из any, отменяются все обещания в этом списке.
Характеристики функции join:
- Функция join создает отдельное обещание, которое выполняется, когда выполняются или завершаются с ошибкой все другие обещания (логическое И). Фактически функция joinподключает обработчики выполнения и ошибок ко всем этим обещаниям и ожидает вызова всех этих обработчиков, прежде чем вызывает все обработчики выполнения, получаемые ей самой.
- Обещание join также сообщает о ходе выполнения в любые предоставленные вами обработчики хода выполнения. Промежуточным результатом в этом случае является массив результатов из отдельных обещаний, выполненных на данный момент.
- Если вы отменяете обещание из join, отменяются все остальные ожидающие обещания.
Кроме any и join, имеется еще два статических метода WinJS.Promise, о которых следует знать, так как они могут оказаться полезными:
- is определяет, является ли произвольное значение обещанием, возвращая логическое значение. Если не вдаваться в подробности, он просто проверяет, что это объект с функцией "then"; наличие функции "done" он не проверяет.
- theneachприменяет обработчики выполнения, ошибок и хода выполнения к группе обещаний (с использованием then), при этом возвращает результаты в виде другой группы значений внутри обещания. Любой из этих обработчиков может иметь значение null.
Параллельные обещания с последовательными результатами
Благодаря WinJS.Promise.joinи WinJS.Promise.any мы получаем возможность работать с параллельными обещаниями, то есть с параллельными асинхронными операциями. Еще раз отмечу, что обещание, возвращаемое функцией join, выполняется, когда выполнены все обещания в массиве. Однако такие обещания обычно выполняются в случайном порядке. А что если бы у вас был набор операций, выполняющихся подобным образом, но вам бы хотелось обрабатывать их результаты строго упорядоченным образом, а именно в порядке следования обещаний в массиве?
Для этого вам необходимо присоединить каждое последующее обещание к функции join из всех предыдущих. А ведь код, с которого мы начали настоящую статью, выполняет именно такую задачу. Еще раз приведем этот код, но переписанный таким образом, чтобы сделать обещания явными. (Предположим, что list— это массив значений определенного рода, использующихся в качестве аргументов для гипотетического асинхронного вызова с созданием обещаний doOperationAsync):
list.reduce(function callback (prev, item, i) { var opPromise = doOperationAsync(item); var join = WinJS.Promise.join({ prev: prev, result: opPromise}); return join.then(function completed (v) { console.log(i + ", item: " + item+ ", " + v.result); }); })
Чтобы понять этот код, нам сначала нужно разобраться, как работает метод reduce этого массива. Для каждого элемента в массиве reduce вызывает аргумент функции, который здесь назван callback и принимает четыре аргумента (из которых только три используются в коде):
- prev — значение, возвращенное из предыдущего вызова callback (для первого элемента оно равно null).
- item — текущее значение из массива.
- i — индекс элемента в списке.
- source — исходный массив.
Для первого элемента в списке мы получаем обещание, которое назову opPromise1. Поскольку prev равно null, мы присоединяем [WinJS.Promise.as(null), opPromise1] . Однако обратите внимание на то, что мы не возвращаем саму функцию join. Вместо этого мы подключаем к этому объединению обработчик выполнения (который я назвал completed) и возвращаем обещание из его then.
Помните, что обещание, возвращенное из then, будет выполнено при возвращении обработчика выполнения. Это значит, что данные, возвращаемые из callback, являются обещанием, которое не исполняется до тех пор, пока обработчик completed первого элемента не обработает результаты из opPromise1. И если вы посмотрите на результат объединения, оно выполняется с объектом, содержащим результаты из обещаний в исходном списке. Это значит, что значение выполнения v будет содержать как свойство prev, так и свойство result, при этом второе является результатом обещания opPromise1.
Вместе со следующим элементом в list callback получает свойство prev, содержащее обещание из предыдущего join.then. После этого мы создаем новое объединение opPromise1.then и opPromise2. В результате такое присоединение не выполняется, пока не будет выполнено обещание opPromise2 и обработчик выполнения для opPromise1 не возвратит значение. Вуаля! Обработчик completed2, который мы подключаем к этому join, не вызывается до тех пор, пока не будет возвращен completed1.
Аналогичные зависимости продолжают создаваться для каждого элемента в списке — обещание из join.then для элемента n не выполняется, пока не будет возвращен completedn**. Таким образом гарантируется вызов обработчиков выполнения в том же порядке, что и в list.
Заключение
В этой статье мы показали, что сами по себе обещания — это просто конструкция кода или соглашение о вызовах, которые можно эффективно использовать. Обещания используются для представления определенного отношения между инициатором, располагающим значениями, которые требуется предоставить в произвольное время в будущем, и потребителем, которому требуется знать, когда эти значения будут доступны. Таким образом, обещания отлично подходят для представления результатов из асинхронных операций и широко используются в приложениях Магазина Windows, написанных на JavaScript. Спецификация для обещаний также допускает объединение последовательных асинхронных операций в цепочки, где каждый промежуточный результат переходит от одного звена к другому.
Библиотека Windows для JavaScript (WinJS) предоставляет надежную реализацию обещаний, которую вы можете использовать для описания своих операций любого вида. Она также предоставляет вспомогательные средства для распространенных сценариев, таких как объединение обещаний для реализации параллельных операций. Благодаря этому WinJS делает работу с асинхронными операциями более удобной и эффективной.
Крэйг Брокшмидт (Kraig Brockschmidt)
Руководитель программы, рабочая группа по экосистеме Windows