Код

Рабочий пример того, что мы собираемся обсудить и построить, доступен ЗДЕСЬ.

Обзор

Проблема достаточно известная. У вас огромное изображение, и вы не хотите использовать встроенные полосы прокрутки, а реализуете прокрутку самостоятельно. У вас есть разные устройства, и вы хотите, чтобы оно работало на всех из них. RxJS спешит на помощь! Этот раздел призван дать вам общее представление о том, что такое Rx, и об одном из многих способов его использования. Если вы такое видите впервые — не пугайтесь и попытайтесь понять хотя бы создайте и подпишитесь на обсервабу. Как только вы почувствуете себя комфортно, попытайтесь понять `switchMap`.

Традиционный подход к перетаскиванию

Общий подход заключается в использовании событий mousedown, mousemove и mouseup вместе с глобальной переменной. В mousedown мы устанавливаем переменную в значение true, а в обработчике mouseup мы устанавливаем ее обратно в false, а между ними, если есть какие-либо события mousemove при запуске мы проверяем соответствующий флаг и выполняем необходимую логику. Существует множество реализаций во всем Интернете, и мы не будем их здесь копать.

С этим простым подходом проблем нет. В большинстве случаев проблем не будет, но по мере роста кода это может выйти из-под контроля и стать кошмаром для поддержки. Представьте, что вы хотите иметь другое поведение при нажатии alt, ctrl или command? В каждом случае нам нужно добавить оператор «если» в каждую ветвь существующих операторов «если». Когда вам приходится иметь дело с разными устройствами и крайними случаями, код становится запутанным.

RxJS

Вы, наверное, уже слышали об этом — это упрощает создание асинхронного кода или кода на основе обратного вызова с использованием программирования в реактивном стиле через Observables. Что это обозначает? Это означает, что мы имеем дело с потоками (представьте себе трубы), которые мы собираем друг в друга, как блоки Lego.

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

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

Выполнение

Жизненный цикл наблюдаемого (т.е. потока):

  1. Мы создаем это
  2. Мы используем на нем разные операторы (преобразование, фильтрация, объединение с другим потоком и т. д.). Это всего лишь этап подготовки, мы не должны заниматься здесь бизнес-логикой!
  3. Мы подписываемся на него (т.е. мы используем его и делаем реальную работу)
  4. Отписываемся (выбрасываем, чтобы сэкономить ресурсы)

Творчество

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

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

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

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

Использование операторов

Наш первый поток более высокой цели называется swipe$. Он сделан из потока wheel$, который мы создали ранее. Обратите внимание, что каждый раз, когда мы вызываем pipe, мы создаем новый поток. Самое приятное в этом то, что мы можем повторно использовать их столько раз, сколько захотим (и в разных блоках Lego), и если мы будем следовать четырем этапам жизненного цикла, которые мы изложили ранее, все должно работать нормально и эффективно (Rx не будет выполняться). ваша логика конвейера, если никто не подписан на ваш поток).

Простой оператор — карта

Снова вернемся к wheel$ — когда мы map обозначаем поток, мы сопоставляем аргумент возникновения события с другой формой. В нашем случае событие колеса (при использовании трекпада или колесика мыши) вместо работы с x и y мы хотим использовать ключевые слова top и left (чуть позже мы увидим, почему мы объединяем его таким образом) и немного увеличьте значения прокрутки. И это все — в конце у нас есть новый поток, на который, если мы подпишемся, мы можем войти в консоль с проекцией аргументов колеса или мы действительно можем сделать что-то полезное, например, обновить поля scrollTop и scrollLeft DOMElement для фактического перемещения. Это. Последнее мы сделаем в ближайшее время. Давайте рассмотрим немного более сложный пример.

оператор — switchMap

Следующий большой блок Lego, который мы хотим создать, — это событие drag. Для этого используем switchMap — посмотрите на картинку здесь. Очевидно, это звучит так, как будто блок лего сопоставляется с другим блоком лего, и в итоге мы должны получить только один уровень блока лего.

Точно так же, как массивы — если мы используем flatMap, мы получим один уровень глубины. Точно так же, если мы сопоставляем вхождение события с другим потоком, который производит некоторые другие вхождения событий, то мы получаем поток, который дает нам часть некоторых других вхождений событий, о которой мы говорили. Но что, если произойдет два события и мы создадим два потока, получим ли мы в два раза больше повторений последнего? Ну, это зависит от оператора — вы можете попробовать увидеть разницу на мраморных диаграммах между mergeMap, concatMap, switchMap.

Назад к switchMap

В тот момент, когда вы получаете вхождение основногопотока, вы начинаете прослушивать внутренний поток, и все вхождения событий попадают в окончательный результирующий поток. Отличие от других операторов *-Map заключается в том, что в тот момент, когда вы получаете еще одно вхождение в основном потоке, вы отказываетесь от прослушивания первого подпотока и начинаете заново, игнорируя любые вхождения, сделанные предыдущим вложенным потоком. Идеально! Это именно то, что мы хотим, давайте попробуем объяснить то же самое в нашем сценарии.

Когда происходит событие mousedown (основной поток), мы хотим начать прослушивание событий mousemove — и это на самом деле наши последние «события перетаскивания», которые создаются из последнего потока. Благодаря замыканию, которое JavaScript позаботится о нас, мы можем использовать координаты, где произошло mousedown, и рассчитать расстояние до того места, где произошло событие mousemove. Как вы уже догадались, мы будем использовать эту дельту для перемещения изображения с помощью scrollLeft и scrollRight так же, как и при свайпе.

оператор — takeUntil

Это довольно легко понять — поток завершается, т.е. перестает генерировать новые значения, и это необратимо! Поскольку наше событие перетаскивания должно перестать создавать значения, в тот момент, когда мышь поднята, мы используем оператор takeUntil, который принимает поток в качестве аргумента. Это означает, что наша внутренняя наблюдаемая больше не работает и никогда не сможет создавать новые значения. К счастью, в тот момент, когда вы выполняете еще одно событие mousedown, мы создаем новый поток из основного потока mousemove (благодаря вызову конвейера), который будет оставаться в живых до тех пор, пока его снова не убьет mouseup!

сенсорныйперетащите

Мы не будем вдаваться в подробности об этом, поскольку техника, которую он использует, очень похожа на предыдущую, и нет никаких новых концепций для рассмотрения. Он использует различные базовые потоки, которые мы изначально создали для мобильных устройств. Ключевая часть заключается в том, что мы получаем поток с той же сигнатурой, что и предыдущие два — он дает нам x и y, которые мы можем использовать для изменения положения прокрутки изображения.

Объедините их всех!

Это третий шаг жизненного цикла, мы делаем что-то полезное, подписываясь на поток (иначе зачем все это готовить?). Поскольку на самом деле у нас есть 3 потока, представляющих взаимодействие с изображением смахиванием, мышью и касанием, они должны обрабатываться одинаково. Мы используем оператор слияния, который принимает любое количество потоков и возвращает нам один поток, который мы можем использовать для обработки за один раз! Подписываемся самым банальным образом, как и раньше, и прокручиваем элемент! Обратите внимание, что мы добавили window.requestAnimationFrame, чтобы фактически избежать перерисовки (вы можете прочитать больше здесь).

Отписаться

Не забудьте очистить после того, как вы закончите, вы не хотите утечек памяти. Функция возвращает массив из нескольких Observable, которые вы можете отменить, когда больше не хотите их слушать. Если вы используете библиотеку на основе компонентов, такую ​​как React, вы хотите использовать componentWillUnmount.

Заключительные слова

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

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