Просто хочу строить свой DOM из своих кирпичей.
С преферансом и поэтессами...
И, если уж на то пошло, может быть что‑то типа: «раз пошла такая пъянка...»
Думаю некторые понимают, что так можно, но — повторение мать учения, и, то есть, никто не мешает и не мешал делать не так, как все привыкли, не брать чей‑то готовый код, и не оставаться в рамках ограничений, наложенных кем‑то на что‑то «просто потому что».
Что мне это даст:
мне больше не нужны подписки, могу просто знать, что какое‑то свойство изменилось
могу устраивать коммуникации с нодами на своё усмотрение
стандартную логику вообще не трогаю и не мешаю ей
Как? Возьму Proxy и буду оборачивать в него всё, что необходимо.
Итого, сначала создадим «базу» для нашего элемента:
const { HTMLElement } = window;
class MyHTMLElement extends HTMLElement {}
customElements.define('my-custom-element', MyHTMLElement);
const myElement = document.createElement('my-custom-element');
console.log('myElement instanceof MyHTMLElement',
myElement instanceof MyHTMLElement); // true
console.log('myElement instanceof HTMLElement',
myElement instanceof HTMLElement); // true
Да, тут штука в том, что нужно использовать customElements.define
и без него никак нельзя, так как оно, в целом, как бы запрещено, без этого мы получим ошибку:
TypeError: Failed to construct 'HTMLElement': Illegal construct
Но в целом нас это никак особо не напрягает, поэтмоу продолжим.
Теперь можно добавить Proxy для отслеживания обращений к свойствам.
const { HTMLElement } = window;
class MyHTMLElement extends HTMLElement {}
const protoProps = {};
Object.setPrototypeOf(protoProps, HTMLElement.prototype);
const proxy = new Proxy(protoProps, {
get(target, prop, receiver) {
const result = Reflect.get(target, prop, receiver);
console.log('get:', prop, result);
return result;
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
console.log('set:', prop, result, value);
return result;
},
});
Object.setPrototypeOf(MyHTMLElement.prototype, proxy);
customElements.define('my-custom-element', MyHTMLElement);
const myElement = document.createElement('my-custom-element');
console.log('myElement instanceof MyHTMLElement',
myElement instanceof MyHTMLElement);
console.log('myElement instanceof HTMLElement',
myElement instanceof HTMLElement);
Теперь убедимся, что всё работает как задумано, создадим простой HTML и добавим необходимую обвязку.
index.html :
<html>
<head>
<style>
#some {
padding: auto;
text-align: center;
border: 1px solid red;
min-height: 100px;
font-size: 7vh;
}
</style>
</head>
<body bgcolor="white">
<div id="some"></div>
<script src="MyHTMLElement.js"></script>
</body>
</html>
MyHTMLElement.js :
const { HTMLElement } = window;
class MyHTMLElement extends HTMLElement {}
const protoProps = {};
Object.setPrototypeOf(protoProps, HTMLElement.prototype);
const proxy = new Proxy(protoProps, {
get(target, prop, receiver) {
const result = Reflect.get(target, prop, receiver);
console.log('get:', prop, result);
return result;
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
console.log('set:', prop, result, value);
return result;
},
});
Object.setPrototypeOf(MyHTMLElement.prototype, proxy);
customElements.define('my-custom-element', MyHTMLElement);
const myElement = document.createElement('my-custom-element');
console.log('myElement instanceof MyHTMLElement',
myElement instanceof MyHTMLElement);
console.log('myElement instanceof HTMLElement',
myElement instanceof HTMLElement);
console.log('render begins');
myElement.innerText = 123;
const renderBox = document.getElementById('some');
renderBox.appendChild(myElement);
console.log('render finish');
Теперь в консоли "видно всё" :

Конечно, теперь нам никто не запрещает играться с ним так, как нам вздумается, например добавить в нехо собственных свойств и наладить собственный прямой канал общения для внешних коммуникаций:
const { HTMLElement } = window;
class MyHTMLElement extends HTMLElement {
communicate(value) {
this.innerHTML = `${this.protoAddition} + ${this.addition} + ${value}`;
}
addition = 'addition';
}
const protoProps = {
protoAddition: 'protoAddition',
};
Object.setPrototypeOf(protoProps, HTMLElement.prototype);
const proxy = new Proxy(protoProps, {
get(target, prop, receiver) {
const result = Reflect.get(target, prop, receiver);
console.log('get:', prop, result);
return result;
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
console.log('set:', prop, result, value);
return result;
},
});
Object.setPrototypeOf(MyHTMLElement.prototype, proxy);
customElements.define('my-custom-element', MyHTMLElement);
const myElement = document.createElement('my-custom-element');
console.log('myElement instanceof MyHTMLElement',
myElement instanceof MyHTMLElement);
console.log('myElement instanceof HTMLElement',
myElement instanceof HTMLElement);
console.log('render begins');
myElement.innerText = 123; // set
const renderBox = document.getElementById('some');
renderBox.appendChild(myElement);
console.log('render finish');
myElement.communicate('message');
console.log(myElement.innerText); // get
Если нужна ссылка на Gist, то Держите .
Надеюсь, что теперь код различных Front-End библиотек будет менее пугающим и загадочным, нет никакой магии, всё банально и не очень сложно.
Конечно, есть и другие API со схожим поведением, Listeners, Observables и т.п. Есть преимущества, есть недостатки. Но если хочется чего-то "простого как топор", то вот.
Спасибо за внимание :-)
Комментарии (7)
nihil-pro
22.08.2025 09:33Только зачем Proxy, если у HTMLElement уже есть методы setAttribute, removeAttributе которые можно переопределять + есть attributeChangedCallbak + MutationObserver чтобы отслеживать вложенные элементы? HTMLElement это очень большой объект, и его проксирование это очень дорогая операция.
wentout Автор
22.08.2025 09:33Да, просто если хочется видеть сами вызовы
.setAttribute
и.removeAttributе
и т.п., то можно и так, думаю. Про размер объекта -- не уверен, что это как-то влияет, объект --это же всего лишь указатель, поэтому какая разница что там "под" прокси. Количество свойств, безусловно влияет если мы хотим их "перебирать", но они же все в этом самом объекте, то есть хешированы и доступ к ним "атомарный". Другое дело, что в самом деле само проксирование -- дорогая операция + поиск свойств вглубину же будет, и чем на более низжем уровне они лежат, тем дольше. А так -- да, конечно, спасибо за дополнение!nihil-pro
22.08.2025 09:33Да, просто если хочется видеть сами вызовы
.setAttribute
и.removeAttributе
и т.п., то можно и такА что еще вы хотите видеть, а самое главное зачем?
Класс HTMLElement наследует класс Element. И в одном и в другом примерно два-три свойства, все остальное пара getter/setter (за исключением readonly свойств, там только getter), следовательно у вас уже есть механизм перехвата чтения/записи без необходимости проксировать
class MyHTMLElement extends HTMLElement { get innerText() { // кто-то прочитал innerText return super.innerText } set innerText(value) { // кто-то хочет зменить innerText return super.innerText = value; } }
Конечно браузеры не могут полностью защитится от таких попыток влезть туда, куда не стоит, но в этом случае они довольно строго предупреждают:
class MyHTMLElement extends HTMLElement { constructor() { super(); return new Proxy(this, {}); } }
TypeError: custom element constructors must call super() first and must not return a different object TypeError: Failed to execute 'createElement' on 'Document': The result must implement HTMLElement interface
То, как вы попытались обойти это ограничение, представляет интерес только для инженеров которые отвечают за реализацию этих ограничений, чтобы они попытались закрыть и эту брешь.
zababurin
Лучше посмотреть на то, что бы сделать базовый абстрактный компонент, от которого наследоваться остальным компонентам. И можно под себя полноценный фреймворк сделать.
wentout Автор
Да, с React Component тоже самое можно провернуть )
zababurin
Ну в реакте это лишнее. Там как правило это стейт менеджер какой нибудь. Да и в целом виртуальное дерево это заменяет.
Реакт это уже фреймворк(знаю что это библиотека) а здесь ты сам создаешь то что требуется. Можно и реакт свой условно собрать. Я хуки(не помню как это называется useEffect useState) на компонентах делал как то.
wentout Автор
Да, там достаточно просто от компонента унаследоваться, и вобоще не иметь дело с остальной их обвязкой с тими props и т.п., если хочется просто как шаблонизатор его использовать чтобы "на коленке" велосипед собрать )