Работа мечты в один клик 💼

💭Мечтаешь работать в Сбере, но не хочешь проходить десять кругов HR-собеседований? Теперь это проще, чем когда-либо!
💡AI-интервью за 15 минут – и ты уже на шаг ближе к своей новой работе.
Как получить оффер? 📌 Зарегистрируйся 📌 Пройди AI-интервью 📌 Получи обратную связь сразу же!
HR больше не тянут время – рекрутеры свяжутся с тобой в течение двух дней! 🚀
Реклама. ПАО СБЕРБАНК, ИНН 7707083893. Erid 2VtzquscAwp
Статья публикуется в переводе, автор оригинального текста Джейсон Найт.
На клиентской стороне фреймворки разрушают юзабельность и доступность, так как многие важные вещи (вроде корзины покупок) просто не могут работать без 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 заставляет вас писать в два раза больше кода чем необходимо для решения задачи (это если не считать библиотечные функции). Это просто абстракция ради абстракции! Хватит уже верить в то, что фреймворки делают разработку лучше!
Комментарии