Влaдислaв Влaсoв, инжeнeр-прoгрaммист в Developer Soft и прeпoдaвaтeль курсa Нeтoлoгии, спeциaльнo для блoгa нaписaл цикл стaтeй o EcmaScript6. В пeрвoй чaсти нa примeрax рaссмoтрeли динaмичeский aнaлиз кoдa в EcmaScript с пoмoщью Iroh.js. В этoй стaтьe рaсскaжeм, кaк рeaлизoвaть oтмeняeмыe Promises.
Прoгрaммa oбучeния: «Прoфeссия frontend рaзрaбoтчик»
Aсинxрoннoсть и плaнирoвщик сoбытий в EcmaScriptКoнцeпция Promise (oбeщaний) — oднa из ключeвыx в сoврeмeннoм EcmaScript. Promise пoзвoляют oбeспeчить последовательное выполнение асинхронных действий за счет организации их в цепочки, которые вдобавок предоставляют перехват ошибок. Современный синтаксис async/await операторов технически также основан на Promise, и является лишь синтаксическим сахаром.
Синтаксический сахар (англ. syntactic sugar) в языке программирования — это синтаксические возможности, применение которых не влияет на поведение программы, но делает использование языка более удобным для человека. — Википедия.
Однако при всей своей богатой функциональности, Promise обладают одним недостатком — не поддерживают возможность отмены запущенного действия. Для того чтобы понять, как обойти это ограничение, необходимо рассмотреть, как возникают и функционируют асинхронные действия в EcmaScript, ведь Promise — лишь обертка для них.
Движок языка EcmaScript, будь это V8 или Chakra, является однопоточным, и позволяет в один момент времени выполнять только одно действие. В браузерной среде довольно современные движки поддерживают технологию WebWorkers, а в node.js можно создать отдельный дочерний процесс, и это позволит параллелизировать выполнение кода. Однако созданный поток исполнения — это независимый процесс, который может обмениваться информацией с создавшим его потоком только посредством сообщений, так что это сама по себе не многопоточная модель.
Вместо этого, традиционный EcmaScript основывается на модели мультиплексирования: чтобы выполнить несколько действий параллельно, они разбиваются на небольшие фрагменты, каждый из которых выполняется относительно быстро и никогда не блокирует поток исполнения. За счет перемешивания таких фрагментов, действия, ассоциированные с ними, фактически выполняются параллельно.
Так как пользовательский код и функции хост-среды, такие как рендеринг визуального интерфейса (UI) веб-страницы, выполняются в одном и том же потоке, то, к примеру, долгий или бесконечный цикл в пользовательском коде приводит к приостановке действий по рендерингу веб-страницы и ее зависанию. Для разделения отрезков времени, в которые будут выполняться те или иные фрагменты кода, применяется планировщик событий — event loop. Каким же образом может возникнуть исполняемый фрагмент в event loop?
Обычный клиентский код выполняет только последовательный набор действий, состоящий из потока исполнения с условиями, циклами и вызовами функций. Для того чтобы осуществить отложенное исполнение, необходимо зарегистрировать клиентскую функцию обратного вызова в хост-среде.
В браузерной среде это сводится, как правило, к одной из трех возможностей: таймеры, события и асинхронные запросы к ресурсам. Таймеры обеспечивают вызов функции по истечении времени (setTimeout), в первом свободном слоте в планировщике событий (setImmediate) или же даже в процессе отрисовки веб-страницы (requestAnimationFrame). События — это реакция на произошедшее действие, как правило, в DOM-модели, и могут инициироваться как пользователем (событие: щелчок по кнопке), так и внутренними процессами отображения UI-элементов (событие: пересчет стилей завершен). В отдельную категорию вынесены запросы к ресурсам, но в действительности они относятся к событиям, с той лишь разницей, что изначальным инициатором является сам клиентский код.
Это наглядно показано на схеме ниже:
Обертка асинхронный действийДалее важно рассмотреть, как вышеуказанные асинхронные действия оборачиваются в Promise. Для того чтобы затронуть максимальное количество аспектов для отмены Promise, следующий код будет сочетать использование таймеров, событий DOM-модели и произвольного клиентского кода, который связывает их. Пример предполагает выполнение AJAX-запроса, возвращающего большой объем данных в CSV-формате, и последующую обработку в потенциально медленной функции в построчном виде для предотвращения зависания основного потока.
function fetchInformation () {function parseRow (rawText) {
/* Some function for row parsing which works very slow */
} const xhrPromise = new Promise ((resolve, reject) = {
const xhr = new XMLHttpRequest ();
xhr.open ('GET', '…/some.csv'); // API endpoint URL with some big CSV database
xhr.onload = () = {
if (xhr.status = 200 && xhr.status 300) {
resolve (String (xhr.response));
} else {
reject (new Error (xhr.status));
}
};
xhr.onerror = () = {
reject (new Error (xhr.status));
};
xhr.send ();
}); const delayImpl = window.setImmediate? setImmediate: requestAnimationFrame;
const delay = () = new Promise (resolve = delayImpl (resolve)) const parsePromise = (response) = new Promise ((resolve, reject) = {
let flowPromise = Promise.resolve ();
let lastDemileterIdx = 0;
let result = []; while (lastDemileterIdx = 0) {
const newIdx = response.indexOf ('n', lastDemileterIdx);
const row = response.substring (
lastDemileterIdx,
(newIdx -1? newIdx — lastDemileterIdx: Infinity)
);
flowPromise = flowPromise.then (() = {
result.push (parseRow (row));
return delay ();
});
lastDemileterIdx = newIdx;
} flowPromise.then (resolve, reject);
}); return xhrPromise.then (parsePromise);
}
В качестве события DOM-модели используется успешное или ошибочное завершение AJAX-запроса, а таймеры обеспечивают последовательную порционную обработку большого объема данных, чтобы предоставить рабочее время UI-потоку. Легко заметить, что с внешней точки зрения такой Promise представляет собой монолитный элемент, на завершении которого вызывающей стороне доступна обработанная база данных в надлежащем формате, или же описание ошибки, если в процессе выполнения произошел сбой.
С точки зрения вызывающей стороны удобно иметь возможность отмены такого Promise, как единого целого. Например, в случае если пользователь закрыл визуальный элемент, которому требовались эти данные для отображения. Однако, с точки зрения внутреннего строения, Promise представляет собой набор синхронных и асинхронных действий, часть из которых возможно уже запущена и завершена. Поскольку эту последовательность определяет произвольный клиентский код, этапы экстренного завершения также должны быть описаны мануальным образом.
Реализация отменяемого PromiseВажно помнить, что прерывание синхронного кода, такого как циклы, не может произойти в принципе, поскольку если код уже выполняется (а движок EcmaScript — однопоточный), то в этот момент не может исполняться никакой другой код, который бы осуществил его прерывание. Таким образом, завершения требуют только действительно асинхронные действия, описанные выше: таймеры, события и обращения к внешним ресурсам.
Функции установки таймеров обладают дуальными операциями для их отмены: clearTimeout, clearImmediate и cancelAnimationFrame соответственно. Для событий DOM-модели достаточно удалить подписку на соответствующую функцию обратного вызова. Также для таймеров можно воспользоваться более простым подходом — предварительно обернуть их в Promise-объект, имеющий мануальный isCancelled-флаг. Если по истечении таймера Promise должен быть отменен, то функция обратного вызова просто не выполняется. В таком случае таймер остается в планировщике, но в случае отмены по его окончании ничего не происходит.
В случае обращения к внешним ресурсам ситуация более сложная: в любом случае можно игнорировать результат операции, выполнив отписку от соответствующего события, но прервать саму операцию не всегда возможно. С точки зрения логики выполнения Promise, это может быть несущественно, однако непрерванная операция потребляет излишние ресурсы.
В частности, метод fetch, призванный на замену классическому XMLHttpRequest для проведения AJAX-запросов, и обеспечивающий сразу возврат Promise-объекта без необходимости дополнительной обертки, не позволяет выполнить отмену запроса. По этой причине для реальной отмены HTTP-запроса необходимо использовать метод abort в объекте XMLHttpRequest.
Итоговый код с поддержкой отмены Promise может выглядеть следующим образом. Для лучшей наглядности показан только изменившийся код, а старый заменен комментарием с многоточием.
function fetchInformation () {/* … */
let isCancelled = false;
let xhrAbort; const xhrPromise = new Promise ((resolve, reject) = {
/* … */
xhrAbort = xhr.abort.bind (xhr);
}); const delayImpl = window.setImmediate? setImmediate: requestAnimationFrame;
const delay = () = new Promise ((resolve, reject) =
delayImpl (() = (! isCancelled? resolve (): reject (new Error ('Cancelled'))))
); /* … */
const promise = xhrPromise.then (parsePromise); promise.cancel = () = {
try { xhrAbort (); } catch (err) {};
isCancelled = true;
} return promise;
}
Поскольку Promise — это обычный объект с точки зрения EcmaScript, то метод cancel легко может быть добавлен в него. Также, поскольку во внешнюю среду возвращается только один результирующий Promise-объект, то метод cancel добавляется только для него, а вся внутренняя логика инкапсулирована в текущем лексическом блоке генерирующей функции.
ИтогиРеализация отменяемого Promise в EcmaScript — сравнительно несложная задача, которая может быть легко выполнена даже для асинхронной цепочки, имеющей внутри нетривиальную логику последовательных вызовов: за счет сохранения флага отмены в объектах и активационных контекстов генерирующих функций. Отмена может быть как поверхностной, когда Promise прерывается с ошибкой и не производит выполнение сторонних эффектов, так и глубокой, когда все инициированные асинхронные операции (таймеры, обращения к внешним ресурсам и прочие) действительно отменяются.
Ключевой аспект отменяемых Promise — необходимость полной мануальной реализации операции отмены. Она не может быть достигнута автоматически, например, за счет реализации собственного класса Promise. Теоретически задача может быть решена при выполнении кода в виртуальной машине, при котором будут записываться все асинхронные действия, инициированные в стеке инициализации Promise и зависимых then-ветках, но это довольно нетривиальная в реализации и малополезная на практике задача.
Читать ещё: «Как правильно оформлять код»
Таким образом, отменяемые Promises в EcmaScript — это всего лишь интерфейсное соглашение, позволяющее прерывать и отменять эффекты от Promise, представляющие собой инкапсулированные цепочки логических действий. В общем же случае концепции отменяемости не существует.
Мнение автора и редакции может не совпадать. Хотите написать колонку для «Нетологии»? Читайте наши условия публикации.
]]>