Статья публикуется в переводе, автор оригинального текста Джейсон Найт.
На клиентской стороне фреймворки разрушают юзабельность и доступность, так как многие важные вещи (вроде корзины покупок) просто не могут работать без 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
Например, вместо бессмысленного 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;
Переменная 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 байт, так что это абсолютная победа.
При этом в ванильной программе вы контролируете каждую строчку кода, а в оригинале основная функциональность библиотеки от вас скрыта.

Калькулятор температуры
Этот пример еще проще и еще менее продуман.
Здесь нам также потребуются вспомогательные "библиотечные" функции (кроме purge).
React-версия:
https://codepen.io/gaearon/pen/WZpxpz?editors=0010
Vanila-версия:
https://codepen.io/jason-knight/full/OJbGmoN
С точки зрения HTML тут сразу все плохо: они используют fieldset
и legend
для того, что должен делать label
. Ужасные люди!
Преобразования
Хуже того, они захаркодили преобразования вместо создания объекта с несколькими преобразованиями. Это тот самый случай, когда объекты и массивы делают код проще и эффективнее. Только посмотрите на это спагетти. Для каждого преобразования своя функция, обработчики и бесконечная цепочка мусора "функционального программирования", которая лишь добавляет накладные расходы. А ведь для всего этого хватило бы одного крошечного обработчика и одного ссылочного объекта!
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.

Методология React заставляет вас писать в два раза больше кода чем необходимо для решения задачи (это если не считать библиотечные функции). Это просто абстракция ради абстракции! Хватит уже верить в то, что фреймворки делают разработку лучше!
Комментарии
Аффтару явно стоило бы хоть раз поработать над крупным проектом на фронте в команде из хотя бы 4 человек, а не крестики-нолики разбирать, прежде чем писать статьи в интернет, глядишь и не нёс бы такую восхитительную чушь. P. S. Сравнивать куски кода в 1Кб и 2Кб - это, конечно, просто за гранью.
та чуваки, чего вы на пацана то наехали, это же переход статьи, не более)
Автор искажает часть фактов, так что посыл заменить React на JQuery можно не воспринимать всерьез. Более того, эта статья очень напоминает английскую статью с таким же посылом :)
Тем не менее, в статье очень правильный вопрос задается: "Решают ли современные библиотеки свои задачи эффективно?". На мой взгляд, ответ будет, что нет и их можно и нужно улучшать. Это не значит, что нужно выбирать JQuery или чистый JS, потому что в этом случае многие вопросы, такие как организация кода ложатся на плечи самого разработчика. Могу сказать из опыта, что получался зоопарк обычно, поэтому я за конкуренцию инструментов и их доработку.
Сейчас очень интересно выглядит Svelte, потому что он как раз предлагает вариант с прямой работой с DOM
Эххх, а если заменить классовые компоненты функциональными, и на хуки, то кода получится меньше чем на ваниле. По поводу хлама - его все таки умные люди делали
Интересно, а автор сможет написать на своем говноКвери простой и поддерживаемый, а главное высокопроизводительный код какого-нибудь мессенджера по типу Facebook ?
Если да, пруфы в студию, пожалуйста! Иначе вы балабол, батюшка.
Вы сравнивает абсолютно разные области применения этих библиотек. Реакт, например, действительно создавался для сложных и больших UI интерфейсов, а jQuery для простых кусков кода, которые не требуют частой перерисовки. А вы тут рассматриваете пример работы калькулятора. Сложилось ощущение, что кроме него в своей жизни ничего сложнее вы не писали..
сразу видно автор нуб и никогда не работал с js
спасибо за альтернативную точку мнения.
Ехал велик через велик Видит велик в реке велик Сунул велик велик в велик Велик велик велик велик
а вообще это самое убедительное опровержение доказываемого, браво!
Сравнивать тёплое с мягким...