cancellations.ru.md
256 lines
| 12.6 KiB
| text/x-minidsrc
|
MarkdownLexer
/ docs / cancellations.ru.md
|
|
r18 | # Cancellations. Отмена асинхронных операций | |
| Использование Promise позволяет организовать обработку результатов работы | |||
| асинхронных фукнций. Ключевые слова async/await позволяют работать с | |||
| асинхронными вызовами в стиле процедурного программирования, хотя по сути это | |||
| событиный подход. При всей своей красоте даннго подхода в нем умышленно | |||
| отсутсвует механизм отмены асинхронной операции, т.е. ее можно начать, но нельзя | |||
| отказаться от результатов ее выполнения, даже если это уже не требуется. | |||
| Примером того, когда может потребоваться отмена является загрузка большого | |||
| файла, при которой пользователю отображается окно хода операции с возможностью | |||
| ее отмены. | |||
| ```ts | |||
| // имеется некоторый HTTP клиент | |||
| let client = new HttpClient(); | |||
| // загружается большой файл, с использованием медленного канала | |||
| let data = await client.getJsonAsync('http://host/large-file.json'); | |||
| ``` | |||
| Как поступить в данной ситуации, прежде всего нужно, чтобы сама операция | |||
| поддерживала возможность отмены, предположим, что для этого есть метод | |||
| `client.abort()`. | |||
| ```ts | |||
| // имеется некоторый HTTP клиент | |||
| let client = new HttpClient(); | |||
|
|
r19 | // отображаем окно с информацией о ходе операции | |
|
|
r18 | let progressView = showProgress("Downloading, please wait..."); | |
| // код оборачивается в try/finally поскольку созданную форму нужно закрыть | |||
| try { | |||
| // загружается большой файл, с использованием медленного канала | |||
| // здесь, в отличии от предыдущего примера, мы не дожидаемся результата, | |||
| // а запоминаем обещание в переменную downloadTask | |||
| let downloadTask = client.getJsonAsync('http://host/large-file.json'); | |||
|
|
r19 | // связываем событие нажатия кнопки с отменой загрузки | |
|
|
r18 | progressView.once('cancel', () => client.abort()); | |
| // ожидаем окончания загрузки данных | |||
| let data = await downloadTask; | |||
| } finally { | |||
| // независимот от результата закрываем форму | |||
| // при этом также происходит ануллирование подписок на события | |||
| progressView.close(); | |||
| } | |||
| ``` | |||
| Технические приведенное решение выглядит не плохо, но проблемы появляются, когда | |||
| требуется организовать отмену нескольких операций, особенно если они вложенные. | |||
| ```ts | |||
| // обновление информации о человеке на форме | |||
| async function updatePersonInfo(info) { | |||
| let client = new RestApiClient(); | |||
| // выплнение нескольких асинхронных операций | |||
| let org = await client.getOrgAsync(info.orgId); | |||
| let city = await client.getCityAsync(info.cityId); | |||
| // обновление содержимого представления | |||
| renderContent({ | |||
| person: info, | |||
| org: org, | |||
| city: city | |||
| }); | |||
| } | |||
| ``` | |||
| Чтобы реализовать возможность отмены такой операции требуется, чтобы в логике | |||
| самой операции была реализована поддержка отмены. Для реализации этого | |||
| потребуется чтобы у операции была информация о запросе отмены, причем данная | |||
| информация относится именно к текущей операции. | |||
|
|
r19 | Информацию о состоянии запроса на отмену назовём **маркер отмены (cancellation | |
|
|
r18 | token)**. Поскольку маркер отмены тесно связан с операцией, его удобно | |
| передавать в виде параметра, тогда код операции будет выглядеть так: | |||
| ```ts | |||
| // обновление информации о человеке на форме | |||
| // ct - маркер отмены | |||
| async function updatePersonInfo(info, ct) { | |||
| let client = new RestApiClient(); | |||
| // выплнение нескольких асинхронных операций | |||
| // маркер отмены просто передается далее по цепочке вызовов, без | |||
| // дополнительных действий | |||
| let org = await client.getOrg(info.orgId, ct); | |||
| let city = await client.getCity(info.cityId, ct); | |||
| // обновление содержимого представления | |||
| renderContent({ | |||
| person: info, | |||
| org: org, | |||
| city: city | |||
| }); | |||
| } | |||
| /////////////////////////////////////////////////////////////////////////////// | |||
| // ... где-то в другом месте | |||
| /////////////////////////////////////////////////////////////////////////////// | |||
| // отображаем окно с информацией о хоже операции | |||
| let progressView = showProgress("Loading, please wait..."); | |||
| // создаем маркер отмены для операции на основе события 'cancel'. | |||
| let ct = new Cancellation(cancel => progressView.on('cancel', cancel)); | |||
| // код оборачивается в try/finally поскольку созданную форму нужно закрыть | |||
| try { | |||
| // асинхронно получаем информацию о человеке | |||
| let data = await getPersonInfo(personId, ct); | |||
| // асинхронно обновляем представление | |||
| await updatePersonInfo(data, ct); | |||
| } finally { | |||
| // независимот от результата закрываем форму | |||
| // при этом также происходит ануллирование подписок на события | |||
| progressView.close(); | |||
| } | |||
| ``` | |||
| Таким образом тот, кто начинает асинхронную операцию заранее определяет как эта | |||
| опреция будет отменена. | |||
| Важно понимать, что для реализации отмены операции | |||
| могут выделаться ресурсы требующие явного освобождения (DOM, таймеры, события), | |||
| об их освобождении по окончанию операции (успешном или нет) должен позаботиться | |||
| инициатор этой операции. `Cancellation` выступает только в роли посредника для | |||
| доставки события отмены операции до конечного получателя, он не отслеживает и | |||
| не освобождает ресурсы, кроме того, асинхронная операция может его попросту | |||
| проигнорировать. | |||
| ## `ICancellation` Маркер отмены операции | |||
| Интерфейс маркера отмены операции. Используется асинхронными операциями, чтобы | |||
| получить оповещение о требуемой отмене. | |||
| ### `isSupported(): boolean` | |||
| Определяет, может ли быть запрошена отмена операции через данный маркер. | |||
| ### `isRequested(): boolean` | |||
| Возвращает текущее состояние запроса на отмену. | |||
| ### `throwIfRequested(): void` | |||
| Если отмена была запрошена, бросает в качестве исключения причину отмены. | |||
| ### `register(cb: (e:any) => void): IDestroyable` | |||
| Метод, зарегистрировать обработчик на запрос отмены. Если отмена была запрошена | |||
| зарегистрированный обработчик будет вызван ровно один раз, независимо от того, | |||
| был ли он зарегистрирован до или после запроса отмены. | |||
| Если отмена уже была запрошена, обработчик будет вызван сразу при регистрации, | |||
| при этом исключения, которые могу возникнуть в обработчике не будут обработаны, | |||
| а передадуться наверх. | |||
| Вызов данного метода приводит к выделению ресурсов, поэтому операция, | |||
| зарегистрировавшая обработчик должна освободить подписку, которую вернет метод. | |||
| ```ts | |||
| async function getAsync(url: string, ct: ICancellation = Cancellation.none) { | |||
| // переменная в которой будет запомнена подписка на запрос отмены | |||
| let reg; | |||
| try { | |||
| // оборачиваем операцию загрузки в Promise | |||
| return await new Promise<string>((resolve, reject) => { | |||
| // объект Xhr | |||
| const xhr = new XMLHttpRequest(); | |||
| xhr.open("GET", url); | |||
| // регистрируем обработчики Promise | |||
| xhr.onload = () => resolve(xhr.responseText); | |||
| xhr.onerror = () => reject(xhr.statusText); | |||
| // отправляем запрос | |||
| xhr.send(); | |||
| // подписываемся на запрос отмены | |||
| reg = ct.register((e) => { | |||
| reject(e); | |||
| xhr.abort(); | |||
| }); | |||
| }); | |||
| } finally { | |||
| if (reg) | |||
| reg.destroy(); | |||
| } | |||
| } | |||
| ``` | |||
| Использование метода `register()` предполагается для организации отмены операций | |||
| не поддерживающих маркеры отмены. | |||
| ## `Cancellation` Источник отмены | |||
| Класс используется для создания маркеров отмены. Позволяет создать маркер при | |||
| начале асинхронной операции и связать его, например, с событием DOM. | |||
| Также маркер можно создавать, когда требуется сложное условие отмены текущей и | |||
| всех нижележещих операций. | |||
| Как правило в большинстве операций достаточно маркера переданного в параметрах, | |||
| этот же маркер может передаваться ниже. | |||
| ### `constructor(exec: (cancel: (reason:any) => void ) => void )` | |||
| Создает новый маркер, при помощи параметра и инициализирует его при помощи | |||
| фукнции, переданной в параметре `exec`. | |||
| ```ts | |||
| let htimer; | |||
| let ct = new Cancellation(cancel => { | |||
| htimer = setTimeout(() => cancel("The request is timed out."), 1000); | |||
| }); | |||
| try { | |||
| let text = await getAsync(url, ct); | |||
| } finally { | |||
| // инициатор должен освобождать ресурсы | |||
| // передача недействительного htimer не приводит ни к каким последствиям | |||
| clearTimeout(htimer); | |||
| } | |||
| ``` | |||
| ## `Cancellation.none: ICancellation` | |||
| Статическое свойство только для чтения, в котором находится специальный токен | |||
| запроса отмены. Этот токен означает, что отмена никогда не может произойти. | |||
| Данный токен рекомендуется использовать как значение по-умолчанию для | |||
| параметров, принимающих токен отмены. | |||
| ```ts | |||
| async function load(url: string, ct: ICancellation = Cancellation.none) { | |||
| ct.throwIfRequested(); | |||
| // ... the rest of method | |||
| } | |||
| ``` |
