Технопарк, осень, 2025 г.
jQuery-style —
Dummy-компоненты — компоненты, которые либо вообще не содержат никакой логики (чисто визуальные компоненты), либо содержат логику, которая глубоко инкапсулирована внутри компонента
Например, компонент "Текст", компонент "Ссылка", компонент "Чекбокс"... Посложнее: компонент "Выезжающее меню", Компонент "Роутер".
Smart-компоненты — компоненты, которые управляют множеством других компонентов, содержат в себе бизнес-логику и хранят какое-то состояние
Компонент "Форма входа". Компонент "Всё приложение"
class TextComponent {constructor(el) { this.el = el || document.createElement('div') }setText(text) {this.text = text;this.el.innerHTML = this.tmpl();}tmpl() {return `<span>${this.text}</span>`;}}
class CounterComponent {constructor(el) {this.el = el || document.createElement('div');this.count = 0; this.text = new TextComponent();setInterval(() => {this.count++;this.text.setText(this.count);this.el.innerHTML = this.tmpl();}, 1000);}...
...tmpl() {return `<h1>Счётчик:</h1>${this.text.tmpl()}`;}}
App — smartRouter — dummyAuthorization — smartForm — dummyTextInput — dummyButton — dummyAbout — smartSimpleText — dummyLink — dummy
innerHTML вставляется в DOM документа
Связывание данных — это процесс, который устанавливает соединение между UI приложения и бизнес-логикой
Различают одностороннее и двустороннее связывание данных — пример
class TextComponent {constructor(el) {this.el = el || document.createElement('div');this.el.innerHTML = this.tmpl();this.text = '';this._textEl = this.el.querySelector('.js-text');}setText(text) {this.text = text;this._textEl.textContent = this.text;}}
class TextInputComponent {constructor(el) {this.el = el || document.createElement('div');this.el.innerHTML = this.tmpl();this.value = '';this._inputEl = this.el.querySelector('input[type=text]');this._inputEl.onchange = () => {this.value = this._inputEl.value.trim();this.emit('change', {value: this.value});};}}
<div><h1>Привет, <span data-bind="textContent:name"></span></h1><input<input type="text"<input placeholder="Сколько вам лет?"<input data-bind="value:age"<input /></div>
class AutoBind {constructor(root) {this.binded = [...root.querySelectorAll('[data-bind]')].map(el => ({el,prop: el['data-dind'].split(':')[0],variable: el['data-dind'].split(':')[1],}));}
getVariable(name) {const entry = this.binded.find(({variable}) => variable === name);return entry ? entry.el[entry.prop] : undefined;}setVariable(name, value) {this.binded.forEach((entry) => {if (entry.variable === name) {entry.el[entry.prop] = value;}});}}
Реактивное программирование — парадигма программирования, ориентированная на потоки данных и распространение изменений.
В обычном мире, чтобы посчитать значение функции, необходимо вызвать её с необходимыми аргументами
В модном и молодёжном реактивном программировании функция сама пересчитается, когда её аргументы изменятся (как, например, в excel)
class AutoReactiveBind {constructor(root, store) {this.store = store;this.binded = ... ;this.binded.forEach(entry => {entry.el.addEventListener('change', () => {this.setVariable(entry.variable, entry.el[entry.prop]);});});}}
getVariable(name) {return this.store[name];}setVariable(name, value) {this.store[name] = value;this.binded.forEach((entry) => {if (entry.variable === name) {entry.el[entry.prop] = value;}});}}
<form data-bind-event="submit:formSubmitted"><input<input type="text"<input placeholder="Сколько вам лет?"<input data-bind-event="input:updateAgeListener"<input /><input type="submit" /></form>
Главная проблема DOM — он никогда не был рассчитан для создания динамического пользовательского интерфейса.
Virtual DOM — это техника и набор библиотек / алгоритмов, которые позволяют нам улучшить производительность на клиентской стороне, избегая прямой работы с DOM путем создания и работы c абстракцией, имитирующей DOM-дерево
// before{tagName: 'div',classes: ['header'],attributes: {hidden: false}}
// after{tagName: 'div',classes: ['header'],attributes: {hidden: true}}
// Real DOM changediv.setAttribute('hidden', 'true')
Такой подход работает быстрее, потому как не включает в себя все тяжеловесные части реального DOM. Но только если мы делаем это правильно. Есть две проблемы:
Когда данные изменяются и нуждается в обновлении. Есть два варианта узнать, что данные изменились:
Что делает этот подход действительно быстрым:
function create(vnode) {// Если создаём текстовый узелif (typeof vnode === 'string') {return document.createTextNode(vnode);}const node = document.createElement(vnode.tag);// Добавляем атрибутыif (vnode.attrs) {for (const [name, value] of Object.entries(vnode.attrs)) {node.setAttribute(name, value);}}
// Создаём детейif (vnode.children) {for (const child of vnode.children) {node.appendChild(create(child));}}// Связываем виртуальный и реальный DOMnode._vnode = vnode;return node;}
const node = vdom.create({tag: 'DIV',attrs: {style: 'background-color: red;','tab-index': 0}});document.body.appendChild(node);
function updateAttrs(node, prevAttrs, attrs) {// Задаём новые значения атрибутовif (attrs) {for (const [name, value] of Object.entries(attrs)) {node.setAttribute(name, value);}}// Удаляем старые атрибуты, которых не встретилось в новыхif (prevAttrs) {for (const name of Object.keys(prevAttrs)) {if (!attrs || !attrs.hasOwnProperty(name)) {node.removeAttribute(name);}}}}
Ключ — специальный строковый атрибут, семантически идентифицирующий элемент среди соседних.
function updateChildren(node, prevChildren, children) {const childLength = children ? children.length : 0;for (let i = 0, j = 0; i < childLength; i++, j++) {const prevVchild = prevChildren && prevChildren[j];const vchild = children[i];const fromString = typeof prevVchild === 'string';const toString = typeof vchild === 'string';if (fromString && toString) {// Если старый и новый элемент -- строки// Внимание! .childNodes, не .children!node.childNodes[i].textContent = vchild;} // else if ( -- Прололжение на следующем слайде
} else if (!fromString && !toString && prevVchild && prevVchild.key === vchild.key) {// Ключи совпадают -- можем обновить оптимальноupdate(node.childNodes[i], vchild);} else {// Если строка превращается в элемент или наоборот// Либо у элементов не совпали ключи// Вставляем новую ноду перед текущей, старая нода при этом "всптывает" в конецnode.insertBefore(create(vchild), node.childNodes[i]);// Сдвигаем индекс сравнения массива старых чайлдовj--;}}
const curChildLength = node.childNodes.length;// Удаляем "лишние" узлыfor (let i = childLength; i < curChildLength; i++) {node.removeChild(node.childNodes[childLength]);}}
const node = vdom.create({tag: 'DIV',});document.body.appendChild(node);dom.update(node, {tag: 'DIV',attrs: {style: 'background-color: black;','tab-index': 2}});
if (typeof vnode.tag === 'function') {// Создаём инстанс компонентаvnode._instance = new vnode.tag(vnode.attrs, vnode.children);// Выполняем его шаблонconst componentVnode = vnode._instance.render();// Создаём DOM по шаблонуconst node = create(componentVnode);// Записываем ссылку на элемент в компонентvnode._instance.el = node;// Сохраняем шаблон компонента для эффективного обновленияnode._originalVnode = node._vnode;// Записываем vdom инстанса компонентаnode._vnode = vnode;return node;}
const node = vdom.create({tag: MyComponentClass,// Эти атрибуты попадут прямо в инстанс компонентаattrs: {value: 1}});
let resultVnode = vnode;if (isComponent) {// Выполняем шаблон компонентаvnode = prevVnode._instance.render();// Переносим инстанс со старой ноды на новуюresultVnode._instance = prevVnode._instance;// Дальше сравнивать будет именно со старым vdom шаблона, а не компонентаprevVnode = node._originalVnode;// Обновляем шаблон компонентаnode._originalVnode = vnode;}updateAttrs(node, prevVnode.attrs, vnode.attrs);updateChildren(node, prevVnode.children, vnode.children);// Обновлем связь с virtual DOMnode._vnode = resultVnode;
updateAttrs(node, prevVnode.attrs, vnode.attrs);updateChildren(node, prevVnode.children, vnode.children);// Обновлем связь с virtual DOMnode._vnode = resultVnode;// Вызываем метод жизненного цикла компонентаif (isComponent) {resultVnode._instance.didUpdate();}
class Component {constructor(attrs, children) {this.attrs = attrs;this.children = children;}/** Вызывается после создания компонента */didCreate() {}/** Вызывается перед обновлением компонента */willUpdate(attrs, children) { /* ... */ }/** Вызывается после обновления компонента */didUpdate() {}/** Вызывается перед уничтожением компонента */willDestroy() {}}
function destroy(node) {const vnode = node._vnode;if (vnode) {const isComponent = typeof vnode.tag === 'function';// Если удаляем компонент, вызовем метод жизненного циклаif (isComponent) {vnode._instance.willDestroy();}// Удаляем детейfor (const child of node.childNodes) {destroy(child);}}}
Веб-компоненты — технология, которая позволяет создавать многократно используемые компоненты в веб-приложениях. Веб-компоненты поддерживаются веб-браузерами напрямую и не требуют дополнительных библиотек для работы . Модель веб-компонентов подразумевает инкапсуляцию и совместимость отдельных элементов
На данный момент частичная поддержка существует в браузерах Chrome, Firefox, Opera и Safari. Для браузеров не поддерживающих веб-компоненты реализованы полифиллы
Веб-компоненты включают в себя 3 технологии:
Shadow DOM — инструмент инкапсуляции HTML. Shadow DOM позволяет изменять внутреннее представление HTML элементов, оставляя внешнее представление неизменным
<input type="range">
// Shadow DOM v0const root = element.createShadowRoot();// Shadow DOM v1const root = element.attachShadow({ mode: 'open' });// const root = element.attachShadow({ mode: 'close' });element.shadowRoot === root; // trueroot.innerHTML = ' ... ';
<!-- html --><div id="element1"><h2>Subheader h2</h2> <p>lorem ipsum</p><h2>Subheader h2</h2> <p>Lorem ipsum dolor sit.</p></div>
const root1 = element1.attachShadow({ mode: 'open' });root1.innerHTML = `<section><h1>Main header</h1><slot></slot></section>`;
<div id="element1"><section><h1>Main header</h1><h2>Subheader h2</h2> <p>Lorem ipsum.</p><h2>Subheader h2</h2> <p>Lorem ipsum dolor sit.</p></section></div>
<!--html--><div id="element2"><span slot="header">Header</span><span slot="content">Lorem ipsum dolor sit.</span></div>
const root2 = element2.attachShadow({ mode: 'open' });root2.innerHTML = `<section><h1> <slot name="header"></slot> </h1><p> <slot name="content"></slot> </p></section>`;
<div id="element2"><section><h1> <span>Header</span> </h1><p> <span>Lorem ipsum dolor sit.</span> </p></section></div>
#shadow-root<style>:host {opacity: 0.4;will-change: opacity;transition: opacity 300ms ease-in-out;}:host(:hover) {opacity: 1;}::slotted(<selector>) { ... }</style>
HTML Templates — это механизм для отложенного рендера клиентского контента, который не отображается во время загрузки, но может быть инициализирован при помощи JavaScript. Содержимое тегов
<template>парсится браузером, но отрабатывает только в момент вставки шаблона в DOM
<!--html--><template id="tmpl"><h3>Заголовок: <slot name="title"></slot></h3><img src="image.png" alt="My Image"><script>alert(1);</script></template>
<!--html--><div id="source"><span slot="title">Hello, World!</span></div>
const target = document.getElementById('source');const tmpl = document.getElementById('tmpl');// two waytarget.appendChild(tmpl.content.cloneNode(true));/* or */target.appendChild(document.importNode(tmpl.content, true));
Ссылка на — пример
<!-- result --><div id="source"><span slot="title">Hello, World!</span><h3>Заголовок: <slot name="title"></slot></h3><img src="image.png" alt="My Image"><script>alert(1);</script></div>
const target = document.getElementById('source');const tmpl = document.getElementById('tmpl');/* or */const root = target.attachShadow({mode:'open'});root.appendChild(tmpl.content.cloneNode(true));
Ссылка на — пример
<!-- result --><div id="source"><h3>Заголовок: <slot name="title">Hello, World!</slot></h3><img src="image.png" alt="My Image"><script>alert(1);</script></div>
Custom Elements API — позволяют создавать и определять API собственных HTML элементов
customElements.define(tagName, constructor, options);class MyHTMLElement extends HTMLElement {}window.customElements.define('my-element', MyHTMLElement);
<!-- PROFIT --><my-element></my-element>
// "super" is not a valid custom element namedocument.createElement('super')instanceof HTMLUnknownElement; // true// "x-super" is a valid custom element namedocument.createElement('x-super')instanceof HTMLElement; // true
x-super:defined {display: block;}x-super:not(:defined) {display: none;}x-super {color: black;}
customElements.define('bigger-img', class extends Image {constructor(width=50, height=50) {super(width * 10, height * 10);}}, {extends: 'img'});
<!-- This <img> is a bigger img. --><img is="bigger-img" width="15" height="20">
const BiggerImage = customElements.get('bigger-img');const image = new BiggerImage(15, 20);console.assert(image.width === 150);console.assert(image.height === 200);customElements.whenDefined('bigger-img').then(() => {console.log('`bigger-img` ready!');});
customElements.define('my-element', class extends HTMLElement {constructor() {super();}connectedCallback() { ... }disconnectedCallback() { ... }adoptedCallback() { ... }...
...get disabled() {return this.hasAttribute('disabled');}set disabled(value) {if (value) {return this.setAttribute('disabled', '');}this.removeAttribute('disabled');}...
...attributeChangedCallback(attrName, oldVal, newVal) { ... }static get observedAttributes() {return ['open', 'disabled'];}});