Рубрики
Без рубрики

Как думать реактивно и анимируют движущиеся объекты с помощью RXJS

В наши дни многие программные системы должны иметь дело с асинхронными поведениями и вопросами, связанными с временем. Непрерывное подключение, распределенные системы, микросервисные архитектуры, облако, не блокирующие платформы – следствие всех этих вещей, что мы как-то должны иметь дело с асинхроникостью и временем. Наши программные системы должны научиться

Автор оригинала: FreeCodeCamp Community Member.

В наши дни многие программные системы должны иметь дело с асинхронными поведениями и вопросами, связанными с временем.

Непрерывное подключение, распределенные системы, микросервисные архитектуры, облако, не блокирующие платформы – следствие всех этих вещей, что мы как-то должны иметь дело с асинхроникостью и временем. Наши программные системы должны учиться, как бороться с потоками событий, которые, по своей природе, асинхронной.

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

Мышление реактивно часто представляет собой проблему, как и какое-либо изменение перспективы. В то же время это может быть проще, чем вы ожидаете. Просто посмотрите на то, что происходит в реальном мире и попробуйте сопоставить его прямо.

В этой статье я стремлюсь показать вам, как применить реактивное и функциональное мышление, чтобы решить очень известную проблему естественным образом: как оживить объект с контролируемым движением. Метафора, которую я буду использовать, состоит из автомобиля, который может ускорить и тормозить, следуя командам, выданным пультом дистанционного управления.

В реализации мы будем использовать RXJS, версию javaScript reaciveX и Teadercript.

Код для полной демонстрации демонстрации можно найти здесь Отказ

Если вам это нравится, Это вторая статья вокруг этих тем.

Быстрый отвод простых оснований динамики

Если вы хотите изменить скорость объекта, вам нужно применить силу к ней, которая, в свою очередь, впечатляет ускорение к тому же объекту. Если вы знаете ценность ускорения А Из объекта вы можете рассчитать вариацию его скорости DV В определенном интервале времени DT с формулой

DV * DT.

Точно так же, если вы знаете скорость V, Тогда вы можете рассчитать вариацию в пространстве DS Через интервал времени DT с формулой

DS * DT.

Заключение: если у вас ускорение А впечатлен объектом, чья первоначальная скорость – V0, Вы можете приблизить скорость объекта в интервале времени DT С его среднем, как это:

средний = (v0 + v1)/2 = (v0 + v0 + dv)/+ a/2 * dt

а затем рассчитать приблизительное изменение пространства DS в том же интервале DT с формулой

DS * * DT + A/2 * DT²

Чем короче временная интервал DT, Чем лучше приближение.

Что «анимация объекта с движением» означает

Если мы хотим анимировать объект с движением, управляемым ускорением, (то есть, если мы хотим имитировать, как объект будет двигаться, если они подвержены силам), мы должны ввести размерность времени.

Мы должны разделить время в интервалах, DT, рассчитайте пространство, пройденное для каждого DT, и показать новую позицию на каждом интервале.

Использование подхода Thing – попросите информацию

Мы можем использовать вышеуказанную функцию, а тянуть От нее информация, которую нам нужна (сколько объект переместился в течение последнего временного интервала DT Учитывая определенное ускорение A и начальная скорость v ). Мы бы взяли результат функции и используете ее для расчета новой позиции, если мы можем каким-то образом запомнить предыдущую позицию.

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

Реактивной способ: подход Push (and Command)

Если вы думаете о транспортном средстве, который кто-то контролируется, вы, вероятно, представляете, что:

  • Автомобиль передает регулярную частоту его положение и скорость к контроллеру
  • Контроллер может изменить ускорение автомобиля (рулевое управление и торможение только изменения в ускорении вдоль оси пространства) для направления движения автомобиля

Такой подход имеет преимущество, чтобы четко отделить обязанности:

  1. Автомобиль отвечает за передачу своего штата в любой момент для любой заинтересованной стороны
  2. Контроллер отвечает за прослушивание данных, передаваемых транспортным средством, и для выдачи правильных команд

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

  • Автомобиль, который передает детали его динамики (например, скорость, положение, направление) – наблюдаемый
  • Контроллер, который слушает такие передачи и выдает команды, чтобы ускорить, замедлить, управлять и тормозным – наблюдателем

Реактивная реализация – RXJS

Разработать решение, мы используем TypeScript в качестве нашего языка программирования и модель ReaciveX через реализацию RXJS. Но концепции могут быть легко перенесены на многие другие языки, поддерживаемые ReactiveX.

Класс MobileObject – представление объектов, которые перемещаются в космосе

Мы собираемся создать наш симулятор, используя реактивные методы с функциональным стилем программирования. Но мы все равно будем использовать хорошие старые концепции объектно-ориентированных (OO), чтобы построить четкую рамку для нашей реализации. Итак, давайте начнем с класса MobileObject:

export class MobileObject {

}

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

Давайте представим г-н наблюдаемый, ядро нашего мобильного устройства

Как мы знаем, контролироваться удаленно, транспортное средство должно постоянно передавать в его данные контроллера о себе, а именно:

  • его текущая скорость
  • его текущая позиция
  • Сколько его положение и скорость варьировались с последнего интервала времени

Это просто Поток данных со временем выделяется транспортным средством. Реактивный эффект Наблюдаемый это способ моделирования Потоки событий, несущих данные со временем Отказ Таким образом, мы можем использовать наблюдаемые для моделирования данных, передаваемых нашим автомобилем.

Наши часы: последовательность временных интервалов

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

С RXJS мы можем создать такой Часы С наблюдаемым использованием следующей функции:

private buildClock(frameApproximateLenght: number) {
  let t0 = Date.now();
  let t1: number;
  return Observable.timer(0, frameApproximateLenght)
    .do(() => t1 = Date.now())
    .map(() => t1 - t0)
    .tap(() => t0 = t1)
    .share();
}
const clock = buildClock(xxx);

Давайте назовем это наблюдаемое Часы Отказ Наше Часы излучает примерно каждый XXX Миллисекунды. Каждое событие, испускаемое Часы будет нести точное количество миллисекунд, прошедших после предыдущего выброса.

Позже, позже, когда говорим о кадрах анимации, почему этот метод создания наблюдаемых временных интервалов удобно. Позже мы также охватим, почему важно использовать Поделиться Оператор при создании Часы Отказ

Рассчитайте изменение скорости и пространства в течение временного интервала

Предположим, что MobileObject подлежит ускорению А Отказ Теперь, когда мы Часы мы можем рассчитать вариацию скорости DV Использование формулы DV * DT. Используя эту формулу и карта Оператор RXJS, мы можем создать наблюдаемый, который излучает изменение скорости с течением времени:

Если мы храним в переменной скорости Вел Время TX Мы можем рассчитать приблизительное изменение в пространстве в следующем интервале времени t (x + 1) С формулой DS * DT + A/2 * DT² Отказ Опять же, используя карта Оператор, мы можем получить наблюдаемый, который излучает вариацию пространства со временем.

Используя тот же подход, мы можем построить наблюдаемый, который выделяет на каждом тике Часы Вся соответствующая информация о динамике MobileObject, начиная с его ускорения А Отказ Мы называем это наблюдаемым Динамика Отказ

Но ускорение может измениться – так что?

Это работает, если мы знаем ускорение А и если А является постоянным.

Что произойдет, хотя если ускорение меняется со временем? Может быть, мы начнем с ускорения A0 , то после периода времени P0 Сила меняет это на A1 Затем после P1 Это меняется на A2 , а потом к A3 , как на следующей диаграмме.

Ускорение выглядит как наблюдаемое, не так ли? Каждое событие представляет собой изменение ускорения MobileObject (то есть тот факт, что новая сила была применена к MobileObject).

Знание A0 Мы можем рассчитать скорость и положение MobileObject на период P0 Использование наблюдаемого Dyn0 , построенный в соответствии с логикой, описанной выше. Когда изменяется ускорение, мы все еще можем рассчитать скорость и положение, но мы должны отказаться от Dyn0 и Переключатель для нового наблюдаемого Dyn1 , который построен с такой же логикой, как Dyn0, Но теперь используя новое ускорение A1 Отказ То же самое переключение повторяется, когда ускорение становится A2 а потом A3 Отказ

Это где оператор Переключатель приходит в удобное. Через Переключатель Мы можем преобразовать Ускорение наблюдаемый в новую версию Динамика наблюдаемый. Это может получить новое значение, исходящее Ускорение начать новый наблюдаемый Dynx, Заполните предыдущий наблюдаемый Dynx-1 И излучают все события, создаваемые различными наблюдателями типа Dynx который он развернулся во время этой обработки. Следующая диаграмма иллюстрирует Переключатель механизм.

Добро пожаловать теперь г-н Тема – педаль ускорителя MobileObject

Для этого надо работать, нам нужно создать педаль акселератора. Это механизм, который позволяет внешним Контроллеры Чтобы изменить ускорение MobileObject.

Ускорение необходимо контролировать, поэтому нам нужен командный механизм.

Чтобы изменить ускорение MobileObject, мы должны вызвать Ускорение наблюдаемое для Emit событий, когда контроллер решает так. Если нам нужно контролировать, когда наблюдаемый излучает, нам нужно посмотреть на Тема другой тип предоставляется RXJS.

Предмет является наблюдаемым, который предлагает следующие методы:

  • Далее (val) : излучает событие с валь в качестве значения
  • Ошибка () : завершает себя с ошибкой
  • полный () : дополняет изящно

Итак, если мы хотим изменить ускорение со временем, мы можем создать Ускорение наблюдаемый как предмет, а затем используйте следующий () метод для излучения события при необходимости.

Оберните все в класс MobileObject

Теперь, когда у нас есть все необходимые детали, мы просто собираем их в когерентный класс MobileObject.

В двух словах, вот как мобильный телефон моделируется в реактивном мире. Есть:

  • Некоторые наблюдаемые, DynamicsX и динамики Из примера, которые выделяют данные о своей динамике вдоль различных размеров пространства (в приведенном выше примере всего 2, X и Y, в двухмерном плане)
  • Некоторые предметы, ускорениеx и ускорение Из примера, которые позволяют контроллерам изменять ускорение по различным размерам
  • внутренние часы, которые устанавливают частоту временных интервалов

В двухмерном пространстве у нас есть 2 разных наблюдаемых, излучающих изменение пространства. Такие наблюдаемые надо Поделиться То же самое Часы Если мы хотим когерентное движение. И Часы сам по себе наблюдаемый. Так что они могут поделиться тем же наблюдаемым, мы добавили Поделиться () Оператор в конце BuildClock () Функция, которую мы описали ранее.

Fall Touch: тормоз

Давайте посмотрим на это очень упрощенно. Если вы хотите остановить или замедлить автомобиль, который движется со скоростью V0 , вы должны подать заявку на автомобиль ускорение в направлении, противоположном его скорости.

Через некоторое время скорость автомобиля станет 0, а в этот момент никакого дальнейшего ускорения на автомобиль не применяется.

Поэтому для получения тормозного эффекта мы должны знать направление мобильных устройств и остановить отрицательное ускорение, когда MobileObject достигает скорости 0.

Знание направления легко. Мы просто должны сделать первое событие, испускаемое DynamicsX или динамики Замечаемое, в зависимости от оси, которую мы заинтересованы, и проверьте, является ли скорость последнего события положительными или отрицательными. Знак скорости – это направление.

directionX = mobileObject.dynamicsX
.take(1)
.map(dynamics => dynamics.vel > 0 ? 1 : -1)

НаправлениеX является наблюдаемым, что издает только одно событие. Излучаемое значение составляет 1, если скорость положительная, -1 в противном случае.

Итак, когда MobileObject принимает команду для тормоза, все это нужно сделать, это получить направление и применить противоположное ускорение, как это:

directionX
.switchMap(
   // BRAKE is a constant of acceleration when mobileObject brakes
   dir => mobileObject.accelerationX.next(-1 * dir * BRAKE)
)

Мы почти на месте. Нам просто нужно убедиться, что как только скорость достигнет 0 или близко к 0, мы удаляем любое ускорение. И именно то, как мы можем получить то, что мы хотим.

directionX
.switchMap(
   // BRAKE is a constant of acceleration when mobileObject brakes
   dir => {
      mobileObject.accelerationX.next(-1 * dir * BRAKE);
      return mobileObject.dynamicsX
      // VEL_0 is a small value below which we consider vel as 0
      .filter(dynamics => Math.abs(dynamics.vel) < VEL_0)
      .do(() => mobileObject.accelerationX.next(0)
      .take(1)
   }
).subscribe()

Здесь после выдачи команды тормозной ускорения мы просто выбираем первое событие DynamicsX наблюдаемый там, где скорость достаточно мала для рассмотрения 0. Затем мы выдаем команду, чтобы применить ускорение, равное нулю. Последний взять (1) Оператор добавлен, чтобы убедиться, что мы немедленно отписываем, поскольку наблюдаемый тормоз завершил свою работу.

Этот код нуждается в некотором доработке для работы действительно гладко, но достаточно передать основы торможения реактивно.

Вернуться к началу: анимация

Все это может выглядеть хорошо, но мы все еще хотим анимировать наш мобильный телефон. Например, мы хотим создать приложение, в котором пользователь может выдавать команды ускорения через 4-кнопочную консоль и см. Соответственно.

Такое приложение действует как контроллер MobileObject и как монитор, чтобы показать анимацию.

Выдача команд

Управление перемещением MobileObject означает, что нам нужно применить ускорение. Приложение браузера может сделать это, используя ускорениеx Субъект, предоставленный MobileObject, как показано на следующем фрагменте.



// mobileObject contains the instance we want to control
const accelerationValue = 100;
pAccX() {
   mobileObject.accelerationX.next(accelerationValue);
}
releaseAccX() {
   mobileObject.accelerationX.next(0);
}

Ускорение 100 применяется, когда кнопка мыши находится вниз, и ускорение установлено значение 0, когда выделяется кнопка мыши, имитируя педаль ускорителя.

Показать анимированное движение

MobileObject открывает DynamicsX и динамики , 2 наблюдаемые, которые постоянно испускают данные о движении вдоль соответствующей оси (например, DeltaSpace, скорость тока, ускорение вдоль x и y). Таким образом, приложение браузера должно подписаться на них, чтобы получить эти потоки событий и изменить положение MobileObject на каждое излучаемое событие, как показано в этом образце фрагмент:

interface Dynamics {deltaVel: number; vel: number; deltaSpace: number; space: number}
const mobileObjectElement = document.querySelector('.mobileobj');
mobileObject.dynamicsX.subscribe(
   (dyn: Dynamics) => {
     const currentPositionX = mobileObjectElement.style.left;
     const deltaSpaceX = dyn.deltaSpace;
     mobileObjectElement.style.left = currentPositionX + deltaSpace;
   }
)

Анимационная рамка

Браузер работает асинхронно, и это невозможно предопределить, когда он готов отображать новый кадр. Анимация или моделирование движения, обеспечивается изменением положения объекта со временем. Гладкая анимация меняет положение на каждом кадре, отображаемом браузером.

RXJS предоставляет Планировщик называется AnimationFrame который обертывает ProwelsimationFrame браузер API. А Планировщик это тип RXJS, который контролирует, когда события, испускаемые наблюдаемым действительно.

Мы можем использовать AnimationFrame и Интервал Статический метод наблюдаемого для создания наблюдаемого, который испускает одно событие каждый раз, когда браузер готов к отображению нового кадра.

Observable.interval(0, animationFrame)

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

Это новый Часы Что мы используем в MobilityObject, чтобы обеспечить поток событий относительно движений ( Dynamicsx и Динамика ). Эти движения синхронизируются, когда браузер готов к отображению нового кадра.

Возможно, вы заметили, что в этом последнем примере кода синтаксис немного изменился. Сейчас мы используем «непроходимые» операторы. Мы не использовали их раньше, так как они ничего не добавляют к нашим рассуждениям. Тем не менее, стоит представлять их, поскольку они представляют новый синтаксис, который вы можете использовать с RXJS 6.

Вы также можете заметить отложить функция. Это функция RXJS, которая возвращает наблюдаемый, но гарантирует, что логика, определенная в функции, передаваемой в качестве параметра для отложить выполняется только тогда, когда наблюдаемый подписан.

Это позволяет нам выполнить BuildClock () Метод в любое время, возможно, при инициализации компонента пользовательского интерфейса. Это также позволяет нам быть уверены, что часы начнут тикать только при подписке и с правильным временем. Более конкретно let.now (); будет выполнен только тогда, когда Часы наблюдаемый подписан.

Последнее, но не менее важное, несколько слов о стиле функционального программирования

В начале нашей дискуссии мы говорили о создании потока данных, представляющих движение MobileObject со временем. Мы позвонили этому Динамика наблюдаемый и использовал следующую логику трансформации:

map(dT => {
  const dV = A * dT;
  vel = vel + dV;
  const dS = vel * dT + A / 2 * dT * dT; 
  space = space + dS;
  return {dV, vel, dS, space};
})

Это предполагает, что мы определили переменные Вел и пространство Где-то так, чтобы они были видны в рамках функции, передаваемой в качестве параметра для карта оператор.

Первое решение, которое может прийти к уму для традиционного программиста OO, состоит в том, чтобы определить такие переменные в качестве свойств класса MobileObject. Но это будет означать хранение информации о состоянии на уровне объекта, которые следует изменить только преобразованием, определенными в рамках карта Оператор показан выше.

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

Вот где находится функциональное программирование на наше спасение.

Функции более высокого уровня

А Функция более высокого уровня это функция, которая возвращает функцию. Имя может напомнить вам о Более высокий уровень наблюдается, которые являются наблюдаемыми, которые выделяют другие наблюдаемые.

Динамика Может быть построен, если у нас есть Часы наблюдаемый, и мы знаем ускорение А Отказ Итак, мы можем сказать, что Динамика это функция Часы наблюдаемое и значение ускорения А Отказ

Мы также можем создать функцию, Dynamicsf , который возвращает функцию дф. Это в свою очередь, когда вызывается, возвращает Динамика наблюдаемый, как показано на фрагменте ниже.

Обратите внимание, что в Dynamicsf, Мы определили переменные Вел и пространство , которые идеально видны изнутри DF сделать наш код последовательным и правильным.

Если у нас есть переменная Часы где мы храним Часы наблюдаемая и переменная ACC где мы храним ценность ускорения А мы можем использовать функцию Dynamicsf, Что мы только что определили, чтобы построить наши Динамика наблюдаемый, как показано на следующем фрагменте.

const dynFunction = dynamicsF();
const dynamics = dynFunction(clock, A);

Ключевой момент в том, что сейчас Dynfunction содержит в своих внутренних переменных Вел и пространство Отказ Он хранит их внутри в своем собственном состоянии, состояние, которое не видно для чего-либо за пределами функции.

Предполагая, что Dynamicsf Это метод класса MobileObject, окончательная версия кода, который создает Динамика наблюдаемый в конструкторе мобильных устройств может быть написан как

const dfX = this.dynamicsF();
this.dynamicsX = this.accelerationX
                     .swithMap(a => dfX(this.clock, a));

При этом мы ограничивали государственную информацию о текущей скорости и пространстве в функцию DFX Отказ Мы также удалили необходимость определить свойства для текущей скорости и пространства в MobilyObject. И мы улучшили повторное использование, так как Dynamicsf () не имеет никакого ссылки на любую ось и может использоваться для расчета обоих DynamicsX и динамики через функциональную композицию.

Применяя функциональный стиль программирования (в этом случае более высокая изоляция), мы получили более высокую безопасность для нашего кода и более высокого использования.

Заключение

Это было довольно долгое путешествие. Мы видели использование некоторых из самых важных операторов RXJS и как субъекты могут быть удобны. Мы также видели, как использовать стиль функционального программирования, чтобы увеличить безопасность нашего кода, а также его повторное использование.

Я надеюсь, что я смог показать, как, используя эффективный подход для мышления к этой проблеме, можно создать программное решение, которое очень естественно отражает модель реальной жизни для объектов, которые удаленно контролируются.

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

Если вам понравилось то, что вы только что прочитали, вы можете быть заинтересованы в Чтение Также эта статья Где я опишу, как создать распределенную систему для управления и отображения в действии нескольких мобильных устройств в распределенной среде.

Весь код кода можно найти здесь Отказ

Я хочу поблагодарить Бен Лет, который вдохновил этот кусок с Один из его разговоров Отказ