⚛ Реакт – хлам, и я вам это докажу!
Современные фронтенд-фреймворки обещают вам быструю разработку, простую интеграцию и избавление от всех возможных проблем. На самом деле обычно вы получаете совсем другое.
Статья публикуется в переводе, автор оригинального текста Джейсон Найт.
На клиентской стороне фреймворки разрушают юзабельность и доступность, так как многие важные вещи (вроде корзины покупок) просто не могут работать без JavaScript и не имеют никакой адекватной "изящной деградации". Хуже того, они скрывают реальные взаимодействия с DOM и добавляют вашим приложениям ненужную сложность.
Конечно, некоторых из этих проблем можно избежать – смотри Gatsby – но это только еще больше запутывает то, что без фреймворков можно сделать проще, чище и понятнее.
Фронтенд-фреймворки в лучшем случае вас дезинформируют, а в худшем – нагло лгут!
Ложь
Во что вы верите?
Прямое взаимодействие с "живым" DOM медленно!
Ха! Нет никакого очевидного преимущества их утомительного "виртуального DOM" перед прямым изменением обычного DOM. Это даже медленнее, потому что требуется проанализировать изменения, прежде чем все равно внести их в живой документ. Просто возьми и измени!
Не храните данные в DOM, это небезопасно!
100% ложь! Вы в любом случае собираетесь поместить их туда, и не имеет значения реально или "виртуально". Это не влияет не только на скорость, но и на безопасность.
DOM слишком сложен для нормальных людей
Серьезно? Вы сравниваете простое дерево объектов с мешаниной кода, свойственной всем фронтенд-фреймворкам? Эти странные утверждения о том, что ванильный код "сложный и непонятный" происходят из какого-то иррационального страха разработчиков перед объектами.
Вам говорят – "ты слишком тупой для всего этого" – и вы верите.
Эти и многие другие утверждения фронтенд-фреймворков в конечном счете сводятся к одному и тому же. Вам предлагают писать больше кода более сложным способом и говорят, что это "проще" и "лучше" чем ванильные эквиваленты. Да кому нужны эти ваши HTML, CSS, JavaScript?
Докажи!
Легко! Возьмем два самых "мясистых" примера из ранних туториалов React: крестики-нолики и калькулятор температуры. В них достаточно логики, и при этом они не являются критичными компонентами как форма контактов или корзина, а значит могут на 100% полагаться на JS без фоллбэков.
Make и другие библиотечные функции
Фреймворки обычно ценят за то, что можно сделать самостоятельно с нуля за пару минут, что мы и сделаем.
Первая функция – make
. Она похожа на знакомый вам create
из React, но может принимать JSON, чтобы создавать за один раз большие DOM-деревья. Это примерно эквивалентно тому, во что компилируется JSX.
Первым параметром передается тег, а вторым либо массив дополнительных инструкций для создания дочерних элементов, либо объект с рядом опциональных свойств:
append
с той же логикой для добавления потомков рекурсивно,attr
с атрибутами,style
для установки стилей,repeat
для создания группы элементов,parent
для указания родительского элемента.
Для большей надежности можно добавить еще поле placement
, чтобы указать конкретное место размещения внутри родителя.
Пример использования
Добавим элемент thead
внутрь таблицы table#test
, а него tr
с несколькими ячейками th
:
Выглядит довольно просто, а главное наглядно. Эта функция использует еще два маленьких вспомогательных метода:
makeAppend
похожа на Element.append
, но принимает массив того, что нужно добавить. Если в потоке данных она встречает массив, то передает его обработку функции make
.
setAttribute
– это прокачанный Element.setAttribute
, способный принимать не только строки. Если он получает массив, объект или функцию, то назначает их напрямую как свойства элемента. Также мы заменяем атрибут className
на class
, как и оригинальный React.
Помимо этого нам понадобится еще одна функция для очистки:
Она удаляет amt
потомков с конца родительского элемента e
. Если передать отрицательное число, то потомки будут удаляться с начала.
Эти 4 маленьких функции покрывают 80% всех задач при работе с DOM.
А теперь начнем!
Tic Tac Toe
Вариант React:
https://codepen.io/gaearon/pen/gWWZgR?editors=0010
Вариант Vanilla:
https://codepen.io/jason-knight/pen/qBqwrwo
Например, вместо бессмысленного div.board-row
в их сгенерированной разметке куда правильнее было бы использовать группу полей fieldset
. А этот state
отслеживает массу ненужных дополнительных данных, в то время как достаточно записывать только ходы – это будет быстрее и чище.
Хуже всего то, что вы просто не видите реальные записи в DOM и должны на 100% полагаться на их код и доверять ему. Говорят, что хранение стейта и виртуальный DOM – это чисто и просто, но это утверждение совсем неочевидно.
Состояние игры
Ванильный вариант начинается с объявления всех переменных, необходимых для отслеживания состояния игры. Если вас беспокоит большое количество глобальных данных, то во-первых код React тоже так делает, а во-вторых – просто оберните все это в IIFE.
Что касается истерических воплей о "побочных эффектах", то запомните уже: сделанное осознанно не является побочным эффектом. Доступ к глобальной области видимости – это не "чистое зло" как вам постоянно твердят. Но если это реально очень вас расстраивает, вы вольны потратить кучу времени, чтобы написать тот же код в ООП-стиле и везде рассовать свой любимый this
.
Переменная squares
– это ссылка на нативную коллекцию fieldset#board.elements
, содержащую элементы игрового поля. Каждый элемент – простой input
, для которого вместо textContent
можно использовать value
. Чуть меньше кода, чуть легче манипуляции. Также устанавливаем обработчик кликов squareClick
– реализация будет чуть позже.
Назначение переменных player
, turn
, winner
, turnHistory
, turnOl
должно быть вполне очевидно. txtPlayer
и txtTurn
содержат ссылки на текстовые узлы. Обратите внимание, конструктор new Text()
– это новый document.createTextNode
.
Создаем дополнительный div
, в котором будет находиться вся информация о состоянии игры:
История ходов
Теперь делаем кнопки для перехода к каждому этапу игры напрямую. Простую функцию turnButton
можно вызывать после каждого хода, а также в начале игры для создания кнопки "Перейти к началу".
Каждая кнопка может хранить значение, и здесь очень удобно, что у элемента button
текст не связан с value
. Именуя аргументы в соответствии с названиями свойств и атрибутов, мы можем использовать краткий синтаксис создания объекта.
Функция restart
возвращает исходное состояние игры:
- очищаем все ячейки игрового поля;
- устанавливаем активного игрока;
- обнуляем победителя и историю ходов.
Теперь создаем кнопку Вернуться к началу и инициализируем игру:
Обработка хода
Теперь нужен обработчик для кликов по сегментам игрового поля:
Просто берем элемент, на котором было вызвано событие, и смотрим, есть ли у него value
. Если нет, то сохраняем в него текущего игрока.
Если история ходов длиннее, чем индекс текущего хода (то есть пользователь решил "переходить"), то нужно стереть все, что было дальше, и записать новый ход.
Наконец проверяем, есть ли победитель. Вдруг после этого хода игроку удалось построить целую линию.
Проверка победителя
Функция для проверки очень простая и гораздо красивее, чем в React-варианте:
- Массив
lines
не хранится внутри функции, а вынесен в глобальную область, поэтому его не нужно создавать каждый раз, тратя на это память. - Благодаря простому сравнению
value
поля с текущим игроком, условие получилось гораздо проще. - Цикл
for...of
позволяет выполнять деструктуризацию прямо цикле. - При необходимости мы возвращаем победителя, в противном случае он аннулируется по умолчанию. Вам не нужно явно возвращать
false
, перестаньте бороться с тем, что JS пытается сделать проще!
Также здесь проверяется последний ход и выполняется переход хода к следующему игроку.
Переходы по истории
Нам еще нужна функция для перехода к конкретному ходу.
В React-версии они на каждом ходу сохраняют все игровое поле. У нас другой подход – перезапуск игры с последовательным подключением выбранных полей.
Это работает намного быстрее, чем вся эта чепуха с состоянием, даже несмотря на то, что мы все сбрасываем и начинаем по сути сначала.
Переход хода
И наконец последняя функция:
Не очень красивая, можно и почистить, но по сравнению с оригиналом вполне себе.
Сравнение
Размер оригинала 3247 байт, а ванильная версия весит 3354 байта. Однако не забывайте, что оригинал еще использует сам React.
Если убрать все комментарии и вспомогательные функции, получится 1956 байт, так что это абсолютная победа.
При этом в ванильной программе вы контролируете каждую строчку кода, а в оригинале основная функциональность библиотеки от вас скрыта.
Калькулятор температуры
Этот пример еще проще и еще менее продуман.
Здесь нам также потребуются вспомогательные "библиотечные" функции (кроме purge).
React-версия:
https://codepen.io/gaearon/pen/WZpxpz?editors=0010
Vanila-версия:
https://codepen.io/jason-knight/full/OJbGmoN
С точки зрения HTML тут сразу все плохо: они используют fieldset
и legend
для того, что должен делать label
. Ужасные люди!
Преобразования
Хуже того, они захаркодили преобразования вместо создания объекта с несколькими преобразованиями. Это тот самый случай, когда объекты и массивы делают код проще и эффективнее. Только посмотрите на это спагетти. Для каждого преобразования своя функция, обработчики и бесконечная цепочка мусора "функционального программирования", которая лишь добавляет накладные расходы. А ведь для всего этого хватило бы одного крошечного обработчика и одного ссылочного объекта!
Сюда гораздо проще добавить новую температурную шкалу чем в оригинал, не так ли? Для примера мы добавили шкалу по Кельвину.
Представление
Для создания и оформления набора полей используем уже знакомую функцию make
:
Здесь у нас fieldset
с правильной легендой, которая отражает реальный смысл всего этого элемента.
Мы используем Object.defineProperty
, чтобы сохранить ссылку на поле ввода в неперечисляемом свойстве объект шкалы.
В итоге получается вот такая разметка (хотя вы и сами уже должны были это понять):
Этот фрагмент добавляется в корневой fieldset
, но ссылка на input
уже сохранена в объекте соответствующей шкалы.
Естественно, нам нужен обработчик события input
:
Получаем input, на котором было вызвано событие и определяем шкалу, к которой он привязан (input.name
). Для всех связанных с ней шкал выполняем преобразования и обновляем значения.
Обратите внимание на свойство valueAsNumber
– удобный способ сразу получить значение в виде числа.
В завершение проверяем, достигнута ли температура кипения воды:
Меняем лишь один крошечный textNode
, а не всю строку!
Вот в общем и все.
Сравнение
React-оригинал весит 2441 байт, а vanilla-версия 2630 байт.
Но опять же – у нас есть комментарии, вспомогательные функции и лишняя шкала по Кельвину.
Удалим это все и получим всего 1243 байта!
При этом код проще для понимания, легче масштабируется и более эффективен, потому что мы вырезали весь этот мусорный виртуальный DOM.
Методология React заставляет вас писать в два раза больше кода чем необходимо для решения задачи (это если не считать библиотечные функции). Это просто абстракция ради абстракции! Хватит уже верить в то, что фреймворки делают разработку лучше!