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 заставляет вас писать в два раза больше кода чем необходимо для решения задачи (это если не считать библиотечные функции). Это просто абстракция ради абстракции! Хватит уже верить в то, что фреймворки делают разработку лучше!

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

  • 1 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 2020 / All rights reserved

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