-
Notifications
You must be signed in to change notification settings - Fork 15
Модуль Async
Современные веб-приложения — это большой клубок асинхронных вызовов. Например, при изменении свойства компонента отправляется асинхронный запрос на сервер; параллельно компонент отслеживает событие прокрутки экрана и обновляет другие свойства, а также генерирует события, которые в свою очередь слушают другие компоненты и т. д. А теперь представьте, что пользователь нажимает на ссылку и переходит на другую страницу: т. к. у нас SPA, то браузер не делает реальный переход, а вместо этого кидает сообщение нашему приложению, которое уже начинает загрузку новых модулей, но ему также нужно отменить/остановить все текущие асинхронные действия, иначе это может привести к очень сложно отлаживаемым ошибкам.
Вопрос в том, как именно реализовать «очистку» асинхронных событий. В самом худшем случае мы в ручном режиме контролируем такие события и самостоятельно чистим их в деструкторе компонента, например,
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
@field()
i: number = 0;
@system()
intervalID?: number;
onScroll() {
console.log('Scroll…');
}
mounted() {
// mounted у компонента может вызваться несколько раз,
// поэтому на всякий случай чистим предыдущий таймер
clearInterval(this.intervalID);
this.intervalID = setInterval(() => { this.i++; }, 1e3);
document.removeEventListener('scroll', this.onScroll);
document.addEventListener('scroll', this.onScroll);
}
beforeDestroy() {
clearInterval(this.intervalID);
document.removeEventListener('scroll', this.onScroll);
}
}
Как видите, ручной контроль асинхронных событий очень быстро приводит к «захламлению» кода и самое главное — очень легко забыть очистить тот или иной ресурс. Для решения этой проблемы в V4 есть специальный модуль Async, который предоставляет набор методов для полного контроля любых асинхронных действий (далее потоков). И хотя сам модуль никак не связан с компонентами (т.е. его можно использовать и в других местах), но все наследники базового блока iBlock имеют свойство .async
с экземпляром класса Async.
Давайте перепишем пример выше с использованием Async:
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
@field()
i: number = 0;
onScroll() {
console.log('Scroll…');
}
mounted() {
// label гарантирует, что при регистрации интервала с такой же меткой
// старый будет автоматически отменен
this.async.setInterval(() => { this.i++; }, 1e3, {
label: 'inc i'
});
// Аналогичная логика с label, но для события
this.async.on(document, 'scroll', this.onScroll, {
label: 'onScroll'
});
}
}
Как видите использование Async сводится к вызову одной из готовых оберток под нужное действие: setTimeout
, setInterval
, request
и т. д. Нам также не нужно думать про очистку ресурсов при уничтожении компонента — это сделает сам Async в деструкторе компонента (эта логика реализована в iBlock). Помимо основных параметров специфичных для каждого из методов, любой из Async методов обладает рядом необязательных параметров, которые дают более полный контроль за происходящим.
{
label?: string | symbol;
group?: string;
join?: boolean | 'replace';
}
С параметром label
мы уже работали в предыдущем примере: он позволяет помечать поток специальной меткой и если до момента его завершения будет попытка установить новый поток с такой же меткой, то старый поток будет уничтожен (для Promise объектов будет выброшено специальное исключение). При этом следует отметить, что различные по типу потоки регистрируются в отдельных песочницах, т. е. одинаковая метка в setTimeout
не может отменить обработчик события. Хорошей практикой считается использование символов для задания меток, т. к. это помогает избежать ситуации, когда мы случайно используем одну метку в дочернем и родительском компоненте для разных по смыслу, но одинаковых по типу потоков. В V4 есть специальной модуль для удобной генерации символов:
import symbolGenerator from 'core/symbol';
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
// На всякий случай экспортируем объект символов,
// чтобы иметь возможность использования в дочернем компоненте
// (хотя это нужно крайне редко)
export const
$$ = symbolGenerator();
@component()
export default class bExample extends iBlock {
@field()
i: number = 0;
onScroll() {
console.log('Scroll…');
}
mounted() {
this.async.setInterval(() => { this.i++; }, 1e3, {
// При первом обращении к свойству будет создан символ,
// который будет возвращаться для всех последующих обращений
label: $$.incI
});
this.async.on(document, 'scroll', this.onScroll, {
label: $$.onScroll
});
}
}
- proxy (метод
proxy
) - promise (методы
promise
,sleep
,wait
,nextTick
,idle
,animationFrame
,promisifyOnce
) - request (метод
request
) - idleCallback (метод
requestIdleCallback
) - timeout (метод
setTimeout
) - interval (метод
setInterval
) - immediate (метод
setImmediate
) - worker (метод
worker
) - eventListener (методы
on
,once
,dnd
) - animationFrame (метод
requestAsimationFrame
)
Параметр join
позволяет изменить стратегию работы label
. По умолчанию старый поток уничтожается, но если задать join: true
, то старый поток будет сохранен, а новый не будет создан: вместо этого метод вернет ссылку на старый поток. Это особенно полезно при работе с обертками над Promise, т. к. в случае использования меток без join
, будет выбрасываться исключение:
import symbolGenerator from 'core/symbol';
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
export const
$$ = symbolGenerator();
@component()
export default class bExample extends iBlock {
created() {
this.async.promise(Promise.resolve(1), {label: $$.promise})
.catch(() => console.log('Abort!'));
this.async.promise(Promise.resolve(2), {label: $$.promise})
.then((r) => console.log(r));
// Abort!
// 2
}
}
При использовании с join: true
:
import symbolGenerator from 'core/symbol';
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
export const
$$ = symbolGenerator ();
@component()
export default class bExample extends iBlock {
created() {
this.async.promise(Promise.resolve(1), {label: $$.promise})
.then((r) => console.log(r));
this.async.promise(Promise.resolve(2), {label: $$.promise, join: true})
.then((r) => console.log(r));
// 1
// 1
}
}
Последнее значение join: 'replace'
работает только для Promise объектов: предыдущий поток будет уничтожен, но вместо генерации исключения обещание начнет ссылаться на новый поток, например,
import symbolGenerator from 'core/symbol';
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
export const
$$ = symbolGenerator ();
@component()
export default class bExample extends iBlock {
created() {
this.async.promise(Promise.resolve(1), {label: $$.promise})
.then((r) => console.log(r));
this.async.promise(Promise.resolve(2), {label: $$.promise, join: 'replace'})
.then((r) => console.log(r));
// 2
// 2
}
}
Параметр group
позволяет задать строковую метку для любых потоков (не обязательного одного типа), а затем использовать методы очистки или приостановки по заданной группе. Это удобно для логического связывания потоков, например, логика Drag&Drop навешивает на элемент события: mousedown
, mouseup
, mousemove
, touchstart
, touchend
, touchmove
— их можно объединить в группу, и потом использовать для очистки.
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
created() {
this.async.setTimeout(() => console.log(1), 500, {group: 'foo'});
this.async.setImmediate(() => console.log(2), {group: 'foo'});
this.async.clearAll({group: 'foo'});
}
}
Когда параметр label
используется вместе с group
, то его действие применяется только для потоков одного типа и одной группы.
import symbolGenerator from 'core/symbol';
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
export const
$$ = symbolGenerator ();
@component()
export default class bExample extends iBlock {
created() {
this.async.promise(Promise.resolve(1), {label: $$.promise, group: '1'})
.then((r) => console.log(r));
this.async.promise(Promise.resolve(2), {label: $$.promise, group: '2'})
.then((r) => console.log(r));
// 1
// 2
}
}
У каждого метода регистрации потока есть метод для очистки, например, setTimeout
/ clearTimeout
и т. д. Также есть универсальный метод clearAll
, который позволяет делать очистку сразу нескольких типов.
Если вызвать метод очистки (например, clearTimeout
) и не указать никаких параметров, то будут очищены все потоки заданного типа. Если вызвать clearAll
без параметров — будут очищены все зарегистрированные потоки всех типов.
Некоторые асинхронные потоки должны выполняться всегда, т. е. не могут быть отменены (например, аналитика). Однако, может возникнуть ситуация, когда поток начал работу, но не успел завершиться из-за вызова деструктора компонента (например, перешли на другую страницу). Чтобы предотвратить такое поведение, необходимо добавить к имени группы потока :zombie
: в таком случае удалить поток получиться только по явному указании идентификатора, group
или label
. Будьте очень осторожны с этой опцией, т. к. она может привести к утечками памяти.
Все методы регистрации возвращают значение, которое можно использовать для точечной очистки, например,
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
created() {
const id1 = this.async.setTimeout(() => console.log(1), 500);
this.async.clearTimeout(id);
const id2 = this.async.setImmediate(() => console.log(2));
this.async.clearAll(id2);
}
}
Метку label
также можно использовать для очистки событий: при этом если использовать clearAll
и label
, то удалятся потоки всех типов с заданной меткой. Также можно использовать group
для сужения поиска.
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
created() {
this.async.setTimeout(() => console.log(1), 500, {label: 'foo'});
this.async.setImmediate(() => console.log(2), {label: 'foo'});
this.async.clearAll({label: 'foo'});
}
}
По аналогии с label
группы могут также использоваться для удаления сразу множества потоков. Также при указании группы можно использовать регулярное выражение и удалять сразу несколько групп, подходящих под условие. Также можно использовать label
для сужения поиска.
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
created() {
this.async.setTimeout(() => console.log(1), 500, {group: 'foo: timer'});
this.async.setImmediate(() => console.log(2), {group: 'foo: immediate'});
this.async.clearAll({group: /foo/});
}
}
Следует понимать, что очистка потоков работает по принципу «здесь и сейчас», т. е. если мы вызвали clearAll
, а затем setTimeout
, то он будет выполнен, т. к. вызвался уже после очистки.
В реальной разработке бывают случаи, когда обработку потоков нужно временно приостановить, а потом возобновить (например, деактивация компонента с сохранением в кеше). Конечно, для решения этой задачи мы можем создать специальный метод регистрации потоков, а затем просто вызывать очистку и регистрацию когда нам нужно. Решение рабочее, но у него есть 2 больших минуса:
- Нам нужно локализировать регистрацию потоков, т. е. уже нельзя просто написать где-то в коде
setTimeout
, т. к. наша программа должна уметь восстаналивать эти отмененные потоки; - Очистка и навешивание обработчиков — это дополнительная логика и процессорное время.
К счастью в Async есть более удобный механизм решения этой проблемы: почти все методы регистрации потоков имеют смежный метод «заглушения» (на данный момент, только worker
не имеет такой API), например, setTimeout
/ muteTimeout
. Также по аналогии с clearAll
есть muteAll
. Интерфейс этих методов полностью совпадает с методами очистки.
Как же работают эти методы? При заглушении потока не происходит его уничтожения, т. е. он по прежнему существует, может делать свою работу или ждать, но это никак не доходит до принимающей стороны, например,
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
created() {
// Переданная функция никогда не выполниться,
//, но сам timeout физически сработает
this.async.setTimeout(() => console.log(1), 500, {group: 'timer'});
this.async.muteTimeout({group: 'timer'});
}
}
Для компонента все также, как если бы мы вызвали очистку. Однако, для каждого метода заглушения существует метод возобновления, например, muteTimeout
/ unmuteTimeout
, muteAll
/ unmuteAll
(зомби потоки также поддерживаются). Интерфейсы этих методов полностью совпадают. Таким образом, когда наш компонент будет готов «ожить», то нам просто нужно вызвать соотвествующие методы unmute
.
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
created() {
this.async.setTimeout(() => console.log(1), 500, {group: 'timer'});
this.async.muteTimeout({group: 'timer'});
this.async.unmuteTimeout({group: 'timer'});
// 1
}
}
Технически заглушить Promise мы можем без проблем, однако если он зарезолвится до возобновления, то он так и останется в «висячем» состоянии. В лучшем случае такой указатель будет уничтожен сборщиком мусора, а в худшем — будет утечка памяти, поэтому нужно быть очень осторожным при заглушении промисов.
Как и в случае с очисткой, заглушение происходит «здесь и сейчас», т. е. если мы вызвали muteAll
, а затем setTimeout
, то он будет выполнен, т к. вызвался уже после заглушения.
Помимо очистки и заглушения существует также приостановка потоков: она очень полезна при работе с обещаниями и одноразовыми событиями. Принцип такой же как и с заглушением: почти все методы регистрации потоков имеют смежный метод «приостановки» (на данный момент, только worker
не имеет такой API), например, setTimeout
/ suspendTimeout
/ unsuspendTimeout
, suspendAll
/ unsupendAll
(зомби потоки также поддерживаются). Отличием приостановки от заглушения является тот факт, что в состоянии suspend
поток записывает все вызовы в специальную очередь, а затем при вызове unsupend
последовательно применяет эти вызовы. Таким образом если Promise был зарезолвен в момент приостановки, то при возобновлении он также зарезолвится и это будет выглядеть так, как будто это произошло только что.
import iBlock, { component, field, system } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
created() {
// Это обещание зарезолвится только после 500мс
this.async.promise(Promise.resolve(1), {group: 'promise'})
.then((r) => console.log(r));
this.async.suspendPromise({group: 'promise'});
this.async.setTimeout(() => this.async.unsuspendPromise({group: 'promise'}), 500);
}
}
Как и в случае с очисткой, приостановка происходит «здесь и сейчас», т. е. если мы вызвали suspendAll
, а затем setTimeout
, то он будет выполнен, т. к. вызвался уже после приостановки.
Как уже говорилось ранее, все наследники iBlock имеют свойство async
, которое содержит экземпляр Async. Также при вызове деструктора компонента будет вызван clearAll
. На события жизненного цикла компонента deactivated и activated автоматически добавлена логика с заглушением (для всех не Promise потоков, кроме тех, у кого в названии группы есть :suspend
) и приостановкой (для всех Promise). Все источники событий parentEvent
, rootEvent
, localEvent
и т. д. упакованы в Async обертки и могут принимать параметры group
, label
и т. д. Кроме того, в Async упакованы и собственные события компонента (on
, off
, once
). Многие методы и декораторы также имеют интеграцию, например, декоратор @watch
:
import iBlock, { component, field, watch } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
@watch({field: 'document: click', group: 'domEvents'})
onClick() {
this.emit('Click!');
}
}
Использование Async делает асинхронное программирование предсказуемым и легко управляемым, поэтому использовать его нужно везде, где есть асинхронные события, даже если они кажутся совсем тривиальными.
localEvent — это специальная событийная шина для внутренних событий компонентов. Ввиду специфики своего использования все события этой шины по умолчанию приостанавливаются (suspend), а не заглушаются (mute) при деактивации компонента. Чтобы отменить это поведение по умолчанию, необходимо добавить к имени группы события :!suspend
, но будьте осторожны, т.к. это может привести к очень сложно отлавливаемым ошибкам.
В Async существуют обертки над всеми видами таймеров JS: setTimeout
, setInterval
, setImmediate
, requestIdleCallback
и requestAnimationFrame
. Все они имеют приблеженный к нативному API, например, значение интервала прокидывается вторым параметром, как и в обычном setInterval
, а функция в requestIdleCallback
получит специальный объект с информацией о затраченом времени. Контекстом функций (this
) передаваемых в обертки будет свойство Async.context
(по умолчанию оно ссылается на сам экземпляр Async, но для компонентов — это ссылка на экземпляр компонента). Также дополнительным параметром все эти методы могу принимать объект настроек Async, который мы уже рассматривали выше. Однако, для этих функций есть 2 дополнительных параметра:
export interface AsyncCb<T extends object = Async> {
(this: T, ctx: AsyncCtx<T>): void;
}
{
onClear?: AsyncCb | AsyncCb[];
onMerge?: AsyncCb | AsyncCb[];
}
Эти функции вызываются при очистке потока или при его слиянии (join: true
). Контекст у них такой же, как и у других функций, а в качестве параметра они принимают специальный мета-объект операции. Можно задавать несколько обработчиков, если передавать их как список.
Для некоторых оберток есть специальные promisify версии:
-
setTimeout
/sleep
; -
setImmediate
/nextTick
; -
requestIdleCallback
/idle
; -
requestAnimationFrame
/animationFrame
.
Обратите внимание, что тип этих оберток — promise
, поэтому для очистки или приостановки необходимо использовать методы обещаний: cancelPromise
, mutePromise
и т. д.
Обертка wait
предоставляет promisify версию setInterval
, которая зарезолвится когда переданная функция вернет true
. По умолчанию интервал проверки равен 15 миллисекунд, но его можно задать с помощью параметра delay
. Wait удобно использовать, когда нужно дождаться некоторого события, но у него нет собственного событийного API.
import iBlock, { component, field, watch } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
async mounted () {
// Promise зарезолвится, когда в DOM дереве появится элемент foo:
// интервал проверки 50 мс
await this.async.wait(() => this.block.element('foo'), {
delay: 50,
label: 'waitFoo'
});
}
}
В Async есть универсальная обертка proxy
, которая подходит для упаковывания любых функций, например,
import iBlock, { component, field, watch } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
mounted() {
const img = new Image ();
img.onload = this.async.proxy(() => console.log('Image loaded!'));
img.src = 'foo.jpg';
}
}
Если вызаваемая функция принимает некоторые аргументы, то они будут переданы в проксируемую функцию. Контекст вызываемой функции идентичен с API таймеров.
Обертка может принимать расширенный набор настроек Async (как и API таймеров), а также еще один дополнительный параметр: single
. Если указать его в false
, то проксируемая функция сможет вызываться несколько раз (по умолчанию функция может вызываться только один раз).
import iBlock, { component, field, watch } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
mounted() {
window.onmessage = this.async.proxy((e) => console.log('Message: ', e), {
single: false
});
}
}
В дополнение к обертке proxy
, в Async есть обертка promise
, которая предоставляет API для регистрации обещаний. Если заглянуть внутрь этой обертки, то можно увидеть, что она сама основана на proxy
, т. е. мы могли бы использовать и его для работы с обещаниями, но все таки promise
предоставляет куда более удобный интерфейс:
import iBlock, { component, field, watch } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
mounted() {
this.async.promise(Promise.resolve(1), {
label: 'myPromise'
}).then((r) => console.log(r));
}
}
Все довольно просто: обертка принимает Promise или функцию, которая возвращает Promise (например async function
) и возвращает новый Promise. Контекст методов обещания идентичен с API таймеров и указывает на Async.context
(по умолчанию он ссылается на сам экземпляр Async, но для компонентов — это ссылка на экземпляр компонента).
Также обертка может принимать стандартный для Async параметры группы, метки, способа объединения и один дополнительный параметр — destructor
. Этот параметр принимает строковое значение названия метода деструктора обещания (если таковой имеется), а по умолчанию Async пытается найти методы abort
или cancel
и использовать их как деструкторы. Зачем нужен этот механизм? Природа обещаний в JS не предполагает стандартного интерфейса отмены, хотя многие библиотеки или стандартные средства браузеры (например, fetch
) реализует такой API и параметр destructor
— это способ сказать Async, что если обещание уничтожается, то нужно также дополнительно вызвать вот этот метод.
Конечно, с точки зрения конечного пользователя, неважно, есть ли у обещания деструктор или нет, т. к. если мы отменяем Promise, то он всё перейдет в состояние rejected со специальным объектом ошибки, но это сделает Promise, который вернула обертка Async, а сам внутренний Promise продолжит выполняться. В некоторых случаях — это может быть весь затратно, например, если мы загружаем на сервер очень большой файл и если не отменить загрузку, то она будет продолжаться, даже если это потеряло смысл. Поэтому в V4 есть специальный модуль Then, который реализует интерфейс Promise с возможностью отмены, а также стандартный модуль Request (изоморфный аналог fetch c некоторыми улучшениями) тоже поддерживает механизм отмены.
Обертка request
— это фасад над оберткой promise
, который помечает такие потоки отдельным типом и вводит ряд вспомогательных методов (cancelRequest
, muteRequest
и т. д.). Дело в том, что походы на удаленный сервер удобно обрабатывать отдельно от остальных промисов, например, метка в sleep
не должна пересекаться с меткой request
, т. к. в этом нет никакого смысла.
Кроме разницы в типе потока между request
и promise
нет различий. Обычно, с оберткой request
в V4 используется модуль Request.
Практически все в браузерном JS завязано на событиях: DOM, события компонентов, сокет события и т. д. И хотя для класса EventEmitter в JS нет какого-то определенного стандартного интерфейса, но как правило он реализует один из перечисленных:
interface EventEmitter1 {
addEventListener(event: string, cb: Function);
removeEventListener(event: string, cb: Function);
}
interface EventEmitter2 {
addListener(event: string, cb: Function);
removeListener(event: string, cb: Function);
}
interface EventEmitter3 {
on(event: string, cb: Function);
off(event: string, cb: Function);
}
Поэтому в Async есть специальная обертка on
, которая предоставляет полиморфный интерфейс для регистрации таких источников событий. Обертка включает в себя методы:
-
on
; -
once
(для одноразовых событий); -
promisifyOnce
(promisify версияonce
); -
dnd
(специальный обработчик Drag&Drop); -
off
; -
muteEventListeners
/unmuteEventListeners
; -
suspendEventListeners
/unsuspendEventListeners
.
Интерфейс on
/once
следущий:
- Ссылка на источник событий;
- Событие (можно указывать несколько через пробел) или массив событий;
- Функция callback для обработки события (принимает аргументами параметры события, контекст идентичен другим функциями в Async);
- Параметры операции:
export interface AsyncCb<T extends object = Async> {
(this: T, ctx: AsyncCtx<T>): void;
}
{
label?: string | symbol;
group?: string;
join?: boolean | 'replace';
onClear?: AsyncCb | AsyncCb[];
onMerge?: AsyncCb | AsyncCb[];
options?: Dictionary;
}
В отличии от других оберток Async, on
по умолчанию задает значение группы по названию прослушиваемого события. Также параметром операции мы можем передать свойство options
(как правило — это объект), который будет передан источнику событий при добавлении слушателя. Например, мы можем использовать это для установки слушателя DOM события на погружении, а не всплытии.
import iBlock, { component, field, watch } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';
@component()
export default class bExample extends iBlock {
created() {
this.async.on(document, 'click', () => console.log('Clicked!'), {
options: {
capture: true
}
});
}
}
Метод once
регистрирует обработчик одноразового события, т. е. событие будет обработано один раз, а затем Async отменит его.
Метод promisifyOnce
возвращает Promise, который зарезолвится, когда сработает заданный обработчик события. Обратите внимание, что тип этой обертки — promise
, поэтому для очистки или приостановки необходимо использовать методы обещаний: cancelPromise
, mutePromise
и т. д.
Метод dnd
создает композицию обработчиков событий, которые участвует в логике Drag&Drop. Т. к. данный метод регистрирует сразу несколько обработчиков, то в качестве результата он возвращает название группы, к которой ассоциированы данные обработчики (сама группа по умолчанию также генерируется с уникальным номером). Помимо указанных выше параметров операции, dnd
может принимать 3 дополнительных:
export type DnDCb<R = unknown, CTX extends object = Async> =
(this: CTX, e: Event, el: Node) => R | Function;
export interface DnDEventOpts<R = unknown, CTX extends object = Async> {
capture?: boolean;
handler: DnDCb<R, CTX>;
}
{
onDragStart?: DnDCb | DnDEventOpts;
onDrag?: DnDCb | DnDEventOpts;
onDragEnd?: DnDCb | DnDEventOpts;
}
Обертка worker
— это самый абстрактный контейнер в Async. Она ничего не регистрирует и не фасадирует, а просто создает связь между экземпляром Async и переданным объектом, а также для каждого нового объекта создает в куче специальный счетчик ссылок (т. е. этот счетчик делится между другими экземплярами Async). Когда объект впервые для данного экземпляра Async передается в worker
, то счетчик ссылок увеличивается на 1, а когда очищается — уменьшается на 1. Если счетчик ссылок равен 0, то Async попытается вызвать деструктор потока: он либо может быть задан явно через параметр destructor
(строка, название вызываемого метода), либо вызовется один из перечисленных методов (если такие имеются):
terminate
destroy
destructor
close
abort
cancel
disconnect
unwatch
Также, если передаваемый в метод worker
объект является функцией, то он и будет считаться деструктором. Следует заметить, что в силу своей абстрактности для передаваемых в worker
объектов не существует механизмов заглушения и приостановки. Возможно, это поведение будет доработано в будущем.
Наверное, обертка worker
используется не очень часто, но есть ряд кейзов, где она незаменима:
-
Допустим мы создали сокет соединение с сервером (глобальный singletone объект), но нам не хочется держать его открытым, если его никто не слушает. Поэтому, компоненты, которые слушают этот сокет, просто пропускают его через Async, а при их уничтожении — счетчик ссылок на наш сокет уменьшится. Если получится, что нет компонентов, которые бы слушали этот сокет, то последний из деструкторов Async вызовет деструктор сокета;
-
У нас есть некоторый Worker, который фоном производит некоторые вычисления, а его результатами пользуются наши компоненты: логика такая же, как и с сокетом — не нужно держать в памяти то, что не используется.