Конечные автоматы на примере плавающей боковой панели (sidebar)

Sergey Vinogradov
2 min readJan 11, 2018

Предыстория

Когда-то давным давно, когда на дворе был javascript 2015… мне понадобилось реализовать вот такую плавающую боковую панель (sidebar):

Особенность в том, что эта панель должна фиксироваться как сверху, так и снизу, если она не помещается в экран

На тот день я нашел несколько плагинов, Sticky-Kit, StickyJS и т.п.

Я честно пытался их подключить… но что-то каждый раз шло не так. То работали как-то странно, то не работали вообще.

В общем, я решил написать свою реализацию — однако задача оказалась совсем не такой простой.

Условий становилось все больше и больше, а код становился все более запутанный — приходилось постоянно бороться с каким-нибудь новым багом.

Я даже отчаялся, что смогу заставить эту панель заработать… правильно! Но уж раз начал — надо доделывать!

В итоге код расчета позиции выглядел примерно так:

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

StickyKit.js
  • Много вложенных условий
  • Логику работы панели понять невозможно
  • Нет уверенности, что все работает правильно
  • Очень сложно что-то поправить, не сломав при этом всего остального

А что можно предложить?

Прошел год — я познакомился с понятием конечного автомата (finite state machine или сокращенно fsm).

И через некоторое время понял, что концепция конечных автоматов идеально подходит для решения задачи с плавающей панелью.

Про теорию конечных автоматов (не скучно и доступно!) можно почитать тут: https://tproger.ru/translations/finite-state-machines-theory-and-implementation/.

Как применить?

Наш конечный автомат будет состоять из:

  1. Множества состояний (START, BOTTOM_FIXED, TOP_FIXED, UNFIXED, FINISH)
  2. Множества событий (каждое событие состоит из набора условий, например, “панель влезает в экран и пересекла точку старта”)
  3. Описания переходов (Событие + Некоторое состояние => Новое состояние)
  4. Действия для каждого состояния (например, “зафиксировать панель сверху” при переходе в состояние TOP_FIXED)
  5. Начального состояния (в нашем случае START)

Для начала определим наши состояния:

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

Поэтому мы сразу перейдем к описанию переходов от одного состояния к другому:

То что находится в функции when, это и есть набор условий — то есть событие.

В переменной d (dimensions) я передаю объект с заранее рассчитанными значениями текущего положения панели, размера экрана и т.д.

Далее опишем действия при переходе в то или иное состояние:

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

Остается создать конечный автомат с нашей конфигурацией и “запустить” его на скролле:

Пользователь будет скроллить страницу, а мы будем пытаться найти доступный переход для текущего состояния панели + некоторых рассчитанных значений (dimensions).

Если переход будет найден — мы перейдем в новое состояние и выполним действие для этого состояния, например, зафиксируем панель.

Дальше все повторится уже с новым состоянием.

Что в итоге?

Хорошая читаемость кода за счет разделения на “смысловые” части (состояния, переходы, действия).

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

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

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

--

--