Share This
Связаться со мной
Крути в низ
Categories
//Реакт – хлам, и я вам это докажу!

Реакт – хлам, и я вам это докажу!

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

reakt hlam i ja vam eto dokazhu def8d5f - Реакт – хлам, и я вам это докажу!

Статья публикуется в переводе, автор оригинального текста Джейсон Найт.

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

На клиентской стороне фреймворки разрушают юзабельность и доступность, так как многие важные вещи (вроде корзины покупок) просто не могут работать без JavaScript и не имеют никакой адекватной “изящной деградации”. Хуже того, они скрывают реальные взаимодействия с DOM и добавляют вашим приложениям ненужную сложность.

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

Фронтенд-фреймворки в лучшем случае вас дезинформируют, а в худшем – нагло лгут!

Ложь

Во что вы верите?

Прямое взаимодействие с “живым” DOM медленно!

Ха! Нет никакого очевидного преимущества их утомительного “виртуального DOM” перед прямым изменением обычного DOM. Это даже медленнее, потому что требуется проанализировать изменения, прежде чем все равно внести их в живой документ. Просто возьми и измени!

Не храните данные в DOM, это небезопасно!

100% ложь! Вы в любом случае собираетесь поместить их туда, и не имеет значения реально или “виртуально”. Это не влияет не только на скорость, но и на безопасность.

DOM слишком сложен для нормальных людей

Серьезно? Вы сравниваете простое дерево объектов с мешаниной кода, свойственной всем фронтенд-фреймворкам? Эти странные утверждения о том, что ванильный код “сложный и непонятный” происходят из какого-то иррационального страха разработчиков перед объектами.

Вам говорят – “ты слишком тупой для всего этого” – и вы верите.

***

Эти и многие другие утверждения фронтенд-фреймворков в конечном счете сводятся к одному и тому же. Вам предлагают писать больше кода более сложным способом и говорят, что это “проще” и “лучше” чем ванильные эквиваленты. Да кому нужны эти ваши HTML, CSS, JavaScript?

Докажи!

Легко! Возьмем два самых “мясистых” примера из ранних туториалов React: крестики-нолики и калькулятор температуры. В них достаточно логики, и при этом они не являются критичными компонентами как форма контактов или корзина, а значит могут на 100% полагаться на JS без фоллбэков.

Make и другие библиотечные функции

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

Первая функция – make. Она похожа на знакомый вам create из React, но может принимать JSON, чтобы создавать за один раз большие DOM-деревья. Это примерно эквивалентно тому, во что компилируется JSX.

         function make(tagName, data) {  var e = document.createElement(tagName);  if (data) {    if (      data instanceof Array ||      data instanceof Node ||      ("object" !== typeof data)    ) return makeAppend(e, data), e;    if (data.append) makeAppend(e, data.append);    if (data.attr) for (      var [name, value] of Object.entries(data.attr)    ) setAttribute(e, name, value);    if (data.style) Object.assign(e.style, data.style);    if (data.repeat) while (data.repeat[0]--) e.append(      make(data.repeat[1], data.repeat[2])    );    if (data.parent) data.parent.append(e);  }  return e; }      

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

  • append с той же логикой для добавления потомков рекурсивно,
  • attr с атрибутами,
  • style для установки стилей,
  • repeat для создания группы элементов,
  • parent для указания родительского элемента.

Для большей надежности можно добавить еще поле placement, чтобы указать конкретное место размещения внутри родителя.

Пример использования

Добавим элемент thead внутрь таблицы table#test, а него tr с несколькими ячейками th:

         make("thead", {  append : [    [ "tr", [      [ "th", { scope : "col", append : "Item" } ],      [ "th", { scope : "col", append : "Quntity" } ],      [ "th", { scope : "col", append : "Unit Price" } ],      [ "th", { scope : "col", append : "Total" } ]    ] ]  ],  parent : document.getElementById("test") ] );     

Выглядит довольно просто, а главное наглядно. Эта функция использует еще два маленьких вспомогательных метода:

         function makeAppend(e, data) {  if (data instanceof Array) {    for (var row of data) {      e.append(row instanceof Array ? make(...row) : row);    }  } else e.append(data); }   function setAttribute(e, name, value) {  if (    value instanceof Array ||    ("object" == typeof value) ||    ("function" == typeof value)  ) e[name] = value;  else e.setAttribute(name === "className" ? "class" : name, value); }     

makeAppend похожа на Element.append, но принимает массив того, что нужно добавить. Если в потоке данных она встречает массив, то передает его обработку функции make.

setAttribute – это прокачанный Element.setAttribute, способный принимать не только строки. Если он получает массив, объект или функцию, то назначает их напрямую как свойства элемента. Также мы заменяем атрибут className на class, как и оригинальный React.

Помимо этого нам понадобится еще одна функция для очистки:

         function purge(e, amt) {  var dir = amt < 0 ? "firstChild" : "lastChild";  amt = Math.abs(amt);  while (amt--) e.removeChild(e[dir]); }      

Она удаляет 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

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

Например, вместо бессмысленного div.board-row в их сгенерированной разметке куда правильнее было бы использовать группу полей fieldset. А этот state отслеживает массу ненужных дополнительных данных, в то время как достаточно записывать только ходы – это будет быстрее и чище.

Хуже всего то, что вы просто не видите реальные записи в DOM и должны на 100% полагаться на их код и доверять ему. Говорят, что хранение стейта и виртуальный DOM – это чисто и просто, но это утверждение совсем неочевидно.

Состояние игры

Ванильный вариант начинается с объявления всех переменных, необходимых для отслеживания состояния игры. Если вас беспокоит большое количество глобальных данных, то во-первых код React тоже так делает, а во-вторых – просто оберните все это в IIFE.

Что касается истерических воплей о “побочных эффектах”, то запомните уже: сделанное осознанно не является побочным эффектом. Доступ к глобальной области видимости – это не “чистое зло” как вам постоянно твердят. Но если это реально очень вас расстраивает, вы вольны потратить кучу времени, чтобы написать тот же код в ООП-стиле и везде рассовать свой любимый this.

         var  lines = [    [ 0, 1, 2 ],    [ 3, 4, 5 ],    [ 6, 7, 8 ],    [ 0, 3, 6 ],    [ 1, 4, 7 ],    [ 2, 5, 8 ],    [ 0, 4, 8 ],    [ 2, 4, 6 ]  ],  player,  squares = make('fieldset', {    repeat : [ 9, "input", {      attr : { onclick : squareClick, type : "button" },    } ],    attr : { id : "board" },    parent : document.body,  }).elements,  turn,  turnHistory = [],  turnOL = make("ol"),  txtPlayer = new Text(),  txtTurn = new Text(),  winner;      

Объявление всех переменных в одном месте очень удобно для работы. Гораздо удобнее, чем их разбрасывание по всему коду. Pascal/Modula/Ada в этом плане просто молодцы.

Переменная squares – это ссылка на нативную коллекцию fieldset#board.elements, содержащую элементы игрового поля. Каждый элемент – простой input, для которого вместо textContent можно использовать value. Чуть меньше кода, чуть легче манипуляции. Также устанавливаем обработчик кликов squareClick – реализация будет чуть позже.

Назначение переменных player, turn, winner, turnHistory, turnOl должно быть вполне очевидно. txtPlayer и txtTurn содержат ссылки на текстовые узлы. Обратите внимание, конструктор new Text() – это новый document.createTextNode.

Создаем дополнительный div, в котором будет находиться вся информация о состоянии игры:

         make('div', {  append : [ txtTurn, " : ", txtPlayer, turnOL ],  parent : document.body });      

История ходов

Теперь делаем кнопки для перехода к каждому этапу игры напрямую. Простую функцию turnButton можно вызывать после каждого хода, а также в начале игры для создания кнопки “Перейти к началу”.

         function turnButton(append, onclick, value) {  make("li", {    append : [ [ "button", { attr : { onclick, value }, append } ] ],    parent : turnOL  }); }       

Каждая кнопка может хранить значение, и здесь очень удобно, что у элемента button текст не связан с value. Именуя аргументы в соответствии с названиями свойств и атрибутов, мы можем использовать краткий синтаксис создания объекта.

Функция restart возвращает исходное состояние игры:

         function restart() { for (var square of squares) square.value = ""; txtPlayer.textContent = player = "X"; winner = false; turn = 0; txtTurn.textContent = "Next Player"; }       
  • очищаем все ячейки игрового поля;
  • устанавливаем активного игрока;
  • обнуляем победителя и историю ходов.

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

         turnButton("Go To Game Start", restart); restart();      

Обработка хода

Теперь нужен обработчик для кликов по сегментам игрового поля:

         function squareClick(e) {  e = e.currentTarget;  if (winner || e.value) return;  e.value = player;  if (turnHistory.length > turn) {    purge(turnOL, turnHistory.length - turn);    turnHistory = turnHistory.slice(0, turn);  }  turnHistory.push(e);  turn++;  turnButton("Go to move " + turn, goToTurn, turn);  calcWinner(); }      

Просто берем элемент, на котором было вызвано событие, и смотрим, есть ли у него value. Если нет, то сохраняем в него текущего игрока.

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

Наконец проверяем, есть ли победитель. Вдруг после этого хода игроку удалось построить целую линию.

Проверка победителя

Функция для проверки очень простая и гораздо красивее, чем в React-варианте:

         function calcWinner() { for (var [a, b, c] of lines) if (  (player == squares[a].value) &&  (player == squares[b].value) &&  (player == squares[c].value) ) {  txtTurn.textContent = "Winner";  return txtPlayer.textContent = winner = player; } if (turn == 9) {  txtTurn.textContent = "Tie";  txtPlayer.textContent = "Game Over"; } else nextPlayer(); }       
  • Массив lines не хранится внутри функции, а вынесен в глобальную область, поэтому его не нужно создавать каждый раз, тратя на это память.
  • Благодаря простому сравнению value поля с текущим игроком, условие получилось гораздо проще.
  • Цикл for...of позволяет выполнять деструктуризацию прямо цикле.
  • При необходимости мы возвращаем победителя, в противном случае он аннулируется по умолчанию. Вам не нужно явно возвращать false, перестаньте бороться с тем, что JS пытается сделать проще!

Также здесь проверяется последний ход и выполняется переход хода к следующему игроку.

Переходы по истории

Нам еще нужна функция для перехода к конкретному ходу.

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

         function goToTurn(e) {  restart();  for (var input of turnHistory) {    input.value = player;    if (++turn == e.currentTarget.value) break;    nextPlayer();  }  calcWinner(); }      

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

Переход хода

И наконец последняя функция:

         function nextPlayer() {   txtPlayer.textContent = player = player === "X" ? "O" : "X"; }      

Не очень красивая, можно и почистить, но по сравнению с оригиналом вполне себе.

Сравнение

Размер оригинала 3247 байт, а ванильная версия весит 3354 байта. Однако не забывайте, что оригинал еще использует сам React.

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

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

reakt hlam i ja vam eto dokazhu 41511cf - Реакт – хлам, и я вам это докажу!

React vs Vanilla

Калькулятор температуры

Этот пример еще проще и еще менее продуман.

Здесь нам также потребуются вспомогательные “библиотечные” функции (кроме purge).

React-версия:

https://codepen.io/gaearon/pen/WZpxpz?editors=0010

Vanila-версия:

https://codepen.io/jason-knight/full/OJbGmoN

С точки зрения HTML тут сразу все плохо: они используют fieldset и legend для того, что должен делать label. Ужасные люди!

Весь этот мусорный JSX только раздувает ваш код и делает вещи сложнее, а не проще!

Преобразования

Хуже того, они захаркодили преобразования вместо создания объекта с несколькими преобразованиями. Это тот самый случай, когда объекты и массивы делают код проще и эффективнее. Только посмотрите на это спагетти. Для каждого преобразования своя функция, обработчики и бесконечная цепочка мусора “функционального программирования”, которая лишь добавляет накладные расходы. А ведь для всего этого хватило бы одного крошечного обработчика и одного ссылочного объекта!

         var   scales = {     celcius : {       fahrenheit : (t) => 32 + t * 1.8,       kelvin : (t) => 273.15 + t     },     fahrenheit : {       celcius : (t) => (t - 32) / 1.8,       kelvin : (t) => 273.15 + ((t - 32) / 1.8)     },     kelvin : {       celcius : (t) => t - 273.15,       fahrenheit : (t) => (t - 273.15) * 1.8 + 32     }   },      

Сюда гораздо проще добавить новую температурную шкалу чем в оригинал, не так ли? Для примера мы добавили шкалу по Кельвину.

Представление

Для создания и оформления набора полей используем уже знакомую функцию make:

         root = make("fieldset", {   append : [ [ "legend", [     "Enter a temperature in any field below for conversion"   ] ] ],   parent : document.body }), boilingText = new Text();      

Здесь у нас fieldset с правильной легендой, которая отражает реальный смысл всего этого элемента.

         function makeTempInput(name, parent) {   Object.defineProperty(scales[name], "input", {     value : make("input", {       attr : {         name,         oninput : onTempInput,         pattern : "[-+]?[0-9]*[.,]?[0-9]+",         type : "number"       },     })   });    make("label", {     append : [ name, ["br"], scales[name].input, ["br"] ],     parent   }); }       

Мы используем Object.defineProperty, чтобы сохранить ссылку на поле ввода в неперечисляемом свойстве объект шкалы.

В итоге получается вот такая разметка (хотя вы и сами уже должны были это понять):

         <label>   fahrenheit<br>   <input     id="temp_fahrenheit"     name="fahrenheit"     oninput="ontempinput();"     pattern="[-+]?[0-9]*[.,]?[0-9]+"     type="number"   ><br> </label>      

Этот фрагмент добавляется в корневой fieldset, но ссылка на input уже сохранена в объекте соответствующей шкалы.

         for (var name in scales) makeTempInput(name, root);      

Естественно, нам нужен обработчик события input:

         function onTempInput(event) {   var input = event.currentTarget;   for (     var [name, method]     of Object.entries(scales[input.name])   ) scales[name].input.value = method(input.valueAsNumber);   boilNoticeUpdate(); }       

Получаем input, на котором было вызвано событие и определяем шкалу, к которой он привязан (input.name). Для всех связанных с ней шкал выполняем преобразования и обновляем значения.

Обратите внимание на свойство valueAsNumber – удобный способ сразу получить значение в виде числа.

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

         function boilNoticeUpdate() {  boilingText.textContent = scales.celcius.input.value >= 100 ? "" : "not"; }       

Меняем лишь один крошечный textNode, а не всю строку!

Вот в общем и все.

Сравнение

React-оригинал весит 2441 байт, а vanilla-версия 2630 байт.

Но опять же – у нас есть комментарии, вспомогательные функции и лишняя шкала по Кельвину.

Удалим это все и получим всего 1243 байта!

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

reakt hlam i ja vam eto dokazhu 3044b18 - Реакт – хлам, и я вам это докажу!

React vs. Vanilla ***

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

Продолжение следует…

  • 2 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован.

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Свежие комментарии

    Рубрики

    About Author 01.

    blank
    Roman Spiridonov

    Моя специальность - Back-end Developer, Software Engineer Python. Мне 39 лет, я работаю в области информационных технологий более 5 лет. Опыт программирования на Python более 3 лет. На Django более 2 лет.

    Categories 05.

    © Speccy 2022 / All rights reserved

    Связаться со мной
    Close