Всем Привет! Зовут меня Майковский Вадим. Я программист-любитель и хочу поделиться с Вами своей находкой.
Странный заголовок, не правда ли?
А зачем вообще обходиться без id? Даже не знаю, но это вполне возможно, и приложение будет живым и вполне себе "реактивным". А всю "магию" при этом творит функция, которую я назвал tokenize.
Конечно же я, ни в коем случае, не настаиваю на отказе от id. "Элементарные" id никому не мешают и tokenize`у тоже. Но если обходиться без id, то как же получать ссылки на DOM элементы, для обращения к ним? Вот для этого и нужна функция tokenize, которая собирает референсы в удобную структуру с ветками, подветками и листьями (ссылками на DOM элементы). А вот как она это делает, мы с Вами сейчас и разберём.
Функция tokenize принимает следующие аргументы:
корневой DOM элемент, начало рекурсии (при старте приложения, обычно это document.body), обязательный аргумент;
Ваш объект-контейнер или экземпляр Вашего объекта/класса, в который будут собираться найденные ветки и листья (что это за ветки и листья будет объяснено в следующем абзаце). Если не указан или логическое ЛОЖЬ – объект-контейнер будет создан;
булевой аргумент. Если логическая ИСТИНА – найденные атрибуты удаляться не будут (про атрибуты читайте в следующем абзаце);
целочисленный аргумент – уровень вложенности для рекурсии. Если не указан или логическое ЛОЖЬ – без ограничений;
строковой аргумент – имя родительского объекта (имя токена).
Рекурсивно вызываемая, на каждом DOM элементе, функция tokenize, путём простого перебора, ищет атрибуты, имена которых маркированы спецсимволами:
* "звёздочка" – токен-ветка (объект-контейнер или экземпляр объекта);
^ "карет" – токен-лист (ссылка на DOM элемент).
Я выбрал именно эти спецсимволы, так-как только эти хорошо заметны (зрительно) в гипертексте.
Имена атрибутов без "*" и без "^" (а в случае "звёздного" атрибута, то так же и без имени пользовательского объекта/класса, при наличии), используется как имя токена. И в родительском объекте создаётся параметр с этим именем, который ассоциируется с объектом или с ссылкой на DOM элемент. Перед ассоциированием имена токенов проверяются на конечную последовательность двух символов "[]" (квадратные скобки). При нахождении оных, они удаляются из имени токена, и в родительском объекте создаётся параметр-массив (если он ещё не создан), в который и добавляется (методом push) новый объект или ссылка на DOM элемент.
В объекте, созданном при нахождении атрибута со звёздочкой, будь то объект-контейнер или экземпляр пользовательского объекта/класса, создается параметр с именем "_" (нижнее подчёркивание), который ассоциируется с DOM элементом. Этот же объект и имя его токена будет передаваться аргументами при рекурсивном вызове tokenize`а.
Предварительно имена "звёздных" атрибутов проверяются на наличие в них имени пользовательского объекта/класса, указанного через знак "-" (минус). При нахождении подобного, tokenize попытается создать экземпляр объекта (если этот объект присутствует в глобальном контексте и доступен через window[искомое]), передав в его конструктор следующие аргументы (по порядку):
ссылка на элемент, которому принадлежит найденный атрибут;
значение "звёздного" атрибута (value);
родительский объект-ветка;
имя родительского объекта (имя токена).
Предупреждаю заранее. Так-как браузеры lowercase`ят атрибуты html элементов, использовать заглавные буквы в именах пользовательских объектов/классов не получится.
Крошечный пример использования
<html>
<head>
<meta charset="UTF-8">
<style>
.menu > * > INPUT {display: none}
.menu > * {background: none; border: none; padding: 5px 10px}
.menu > BUTTON:hover {background-color: rgba(0,0,0,.05)}
.menu > *:has(input:checked) {border-bottom: solid 2px black; border-top: solid 2px transparent}
.tab {padding: 50px; border-top: solid 1px grey}
.tab > DIV {width: 100%}
.tab:has(> input:not(:checked)) {display: none}
</style>
<script>
function tokenize(a, b, c, d, e){
let f = a
if(!b) b = {}
loop:{
for(const g of a.attributes){
let h = g.name, i
switch(h[0]){
case '*':
if(!c) a.removeAttribute(h)
if((i = h.indexOf('-')) > 1){
const j = h.slice(i + 1)
if(f = window[j])
f = new f(a, g.value, b, e)
else
console.error('Объект "' + j + '" (из атрибута "' + h + '") отсутствует в глобальном контексте!')
h = h.slice(0, i)
}
if(f && f != a) f._ = a
else f = {_: a}
case '^':
if(!c) a.removeAttribute(g.name)
let k = h.slice(1)
if(k.slice(-2) == '[]'){
k = k.slice(0, -2)
if(!b[k] || !Array.isArray(b[k])) b[k] = []
b[k].push(f)
}else
b[k] = f
if(f != a) e = k
break loop
}
}
}
if(d >= 0) d--
if(!(d < 0)){
if(f == a) f = b
for(const g of a.children) tokenize(g, f, c, d, e)
}
return b
}
function menubtn(a, b, c, d){
const r = document.createElement('input')
a.appendChild(r)
r.type = 'radio'
r.name = d
this.click = a.onclick = e => {
app.tab[b].checked = r.checked = true
}
}
</script>
</head>
<body full style="padding: 50px">
<div *menu class="menu">
<button *a[]-menubtn="0">Вкладка 1</button>
<button *a[]-menubtn="1">Вкладка 2</button>
</div>
<div class="tab">
<input ^tab[] type="radio" name="tabs" hidden>
<div *tab0>
<p ^p style="color:blue">Содержимое вкладки 1</p>
</div>
</div>
<div class="tab">
<input ^tab[] type="radio" name="tabs" hidden>
<div *tab1>
<p ^p style="color:red">Содержимое вкладки 2</p>
</div>
</div>
<script>
const app = tokenize(document.body)
app.menu.a[0].click()
</script>
</body>
</html>
В приведённом примере, tokenize "проплывая" "проходя" по DOM дереву поместит в константу "app" главный объект-контейнер, в который, в свою очередь, поместит три объекта-контейнера с именами параметров-токенов: "menu", "tab0", "tab1" и один массив под именем "tab" в который поместит ссылки на два DOM элемента "input". В контейнере "menu" tokenize создаст массив под именем "a", в который поместит два экземпляра объекта "menubtn" ассоциированных с двумя кнопками в DOM дереве.
Переключать вкладки, в приведённом примере, можно программно, путём вызова метода "click", в одном из двух экземпляров объектов "menubtn". Что, собственно, и видно на приведённом примере. Строчка app.menu.a[0].click() активирует первую вкладку. А что бы, к примеру, поменять текст во второй вкладке, можно сделать так app.tab1.p.innerHTML = "и без длиннющего document.getElementById".
Более расширенный пример, с попами (ой, прошу прощения), с popmenu, с модальными окнами и с календариком смотрите здесь https://github.com/Mickommic/tokenize/tree/main
Возможности tokenize не ограничены только лишь его начальным вызовом, при старте приложения. К примеру, если во время работы приложения, каким‑либо способом было получен новый контент и вставлен где‑либо в DOM дереве через «innerHTML», то этот новый контент может быть «токенизирован» отдельно, путём вызова tokenize на том элементе, куда был вставлен новый контент.
Что ж, на этом, пожалуй, всё. Позвольте откланяться. Всех с Новым 2026-м годом. И да прибудет с Вами сила (алгоритмов).
Интересное наблюдение, в этой статье 26 раз встречается слово «объект».
Комментарии (11)

pravosleva
04.01.2026 22:13Это интересный эксперимент, но, кмк, для простого лендинга (или другого контента со статической разметкой). Работа построенная на мутациях DOM напрямую в растущем проекте ведёт к росту императивного кода и снижению уровня абстракции и его рассеиванию (а должно быть наоборот). Да, здесь работа сводится к работе с данными - это хорошо, но я подозреваю, отказавшись от React (как заявлено в заголовке), Вы должны будете прийти к его альтернативе как минимум т.к. обновление данных - это ещё не всё, также нужна возможность описывать поведение аппки, чтоб оставаться на высоком уровне абстракции.

mickommic Автор
04.01.2026 22:13Полностью с Вами согласен, и не в коем случае не призываю отказываться от React. Я лишь хотел показать, что у "реактивности" всё же есть альтернатива.

Zukomux
04.01.2026 22:13И в каком месте тут реактивность? Обычный прямой вызов функции. Вы лучше покажите как при клике кнопки в табе уходит запрос, его ответ корректно обрабатывается и потом отображается в виде датасета или таблицы, которая умеет работать с этим же запросом - сортировка/фильтрация, и вот тогда поговорим о реактивности)

mickommic Автор
04.01.2026 22:13Ну зачем же так усложнять то. Простой клик и датасет с таблицами. Неужели прямого вызова функции недостаточно для реактивности?

pravosleva
04.01.2026 22:13Кстати, да. Под реактивностью обычно хочется видеть как минимум Proxy-объект. Это несложно, но это должно быть абстрагировано от UI - отсюда следует, что это не проблема управления вкладками или прочим UI, а скорее, подписка на изменение состояния (и должно быть описано отдельно)

mickommic Автор
04.01.2026 22:13Хочу внести ясность и развеять недопонимание. Многие могли подумать, что я против React. Уверяю Вас, я совершенно не против React. Продукты на React реально качественные, какие и нужны для бизнес-задач. И если Вы скажете, что моя идея не для "прода", то Вы совершенно правы, и я с Вами согласен. Но я, собственно, никогда не позиционировал и не ориентировал свою идею для продакшена, и вообще ни к чему не призывал. Я всего лишь описал возможность, что можно сделать вот так и это работает.
Как правильно выразился pravosleva – это эксперимент, просто интересный эксперимент.
По поводу "мутаций DOM". Если Вас пугает это словосочетание, то можно, к примеру, сделать и так
<div token-name="Menu" token-type="container" class="menu"> <button token-name="a[]" token-type="object" token-object="MenuBtn" value="0">Вкладка 1</button> <button token-name="a[]" token-type="object" token-object="MenuBtn" value="1">Вкладка 2</button> </div>Это позволит избавиться от цикла перебора атрибутов, и позволит использовать заглавные буквы как в именах токенов, так и в именах объектов/классов.
PS. Пожалуй, я ошибся с заголовком, уж слишком "кричащим" он получился.
alex1t
А как в таком случае быть с, например, Google Tag Manager? Он конечно многое умеет, но проще работать с ним зная конкретный id элемента, для которого надо снять аналитику (клики хотя бы).
mickommic Автор
Как и написано в статье, я не призываю отказываться от id. Разумеется id никому не мешают и tokenize`у тоже.