Просто хочу строить свой 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)


  1. zababurin
    22.08.2025 09:33

    Лучше посмотреть на то, что бы сделать базовый абстрактный компонент, от которого наследоваться остальным компонентам. И можно под себя полноценный фреймворк сделать.


    1. wentout Автор
      22.08.2025 09:33

      Да, с React Component тоже самое можно провернуть )


      1. zababurin
        22.08.2025 09:33

        Ну в реакте это лишнее. Там как правило это стейт менеджер какой нибудь. Да и в целом виртуальное дерево это заменяет.

        Реакт это уже фреймворк(знаю что это библиотека) а здесь ты сам создаешь то что требуется. Можно и реакт свой условно собрать. Я хуки(не помню как это называется useEffect useState) на компонентах делал как то.


        1. wentout Автор
          22.08.2025 09:33

          Да, там достаточно просто от компонента унаследоваться, и вобоще не иметь дело с остальной их обвязкой с тими props и т.п., если хочется просто как шаблонизатор его использовать чтобы "на коленке" велосипед собрать )


  1. nihil-pro
    22.08.2025 09:33

    Только зачем Proxy, если у HTMLElement уже есть методы setAttribute, removeAttributе которые можно переопределять + есть attributeChangedCallbak + MutationObserver чтобы отслеживать вложенные элементы? HTMLElement это очень большой объект, и его проксирование это очень дорогая операция.


    1. wentout Автор
      22.08.2025 09:33

      Да, просто если хочется видеть сами вызовы .setAttribute и .removeAttributе и т.п., то можно и так, думаю. Про размер объекта -- не уверен, что это как-то влияет, объект --это же всего лишь указатель, поэтому какая разница что там "под" прокси. Количество свойств, безусловно влияет если мы хотим их "перебирать", но они же все в этом самом объекте, то есть хешированы и доступ к ним "атомарный". Другое дело, что в самом деле само проксирование -- дорогая операция + поиск свойств вглубину же будет, и чем на более низжем уровне они лежат, тем дольше. А так -- да, конечно, спасибо за дополнение!


      1. 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

        То, как вы попытались обойти это ограничение, представляет интерес только для инженеров которые отвечают за реализацию этих ограничений, чтобы они попытались закрыть и эту брешь.