Не стоит воспринимать этот материал как руководство к действию, это только базовые принципы работы с VDOM, которые используют большинство. Реализация будет максимальной простой и не учитывающей многих факторов и проблем кросбразерности. Главная цель, показать, что никакой магии нет и сам принцип очень прост.
function Button({name, text, onTap}) {
return (
<button className="button" name={name} onClick={onTap}>
{text}
</button>
);
}
function Button({name, text, onTap}) {
return React.createElement(
"button", // Element
{className: "button", name, onTap}, // attributes
text // children
);
}
function Button({name, text, onTap}) {
return {
tag: "button",
attrs: {class: "button", name, onTap},
children: [text]
};
}
<div class="app"> <h1>Hi, %username%!</h1> <button name="enter" class="button">Enter</button> </div> ↓(click + prompt)↓ <div class="app"> <h1>Hi, VD!</h1> <button name="exit" class="button">Exit</button> </div>
import {Component} from "./vdom";
import Button from "./Button";
class App extends Component {
handleEvent(evt) {
#! const name = evt.currentTarget.name === "enter" ? prompt("Name?") : null;
#! this.setState({name});
}
render() {
#! const {name} = this.state;
#!+ return {
tag: "div",
attrs: {className: "app"},
children: [
#! {tag: "h1", children: [`Hi, ${name || '%username%'}!`]},
#!+ {tag: Button, attrs: {
#!+ name: name ? "enter" : "exit",
#!- text: name ? "Enter" : "Exit",
#! onClick: this,
#!- }},
]
#!- };
}
}
import {render} from "./vdom";
import App from "./App";
const container = document.getElementById("root");
render(container, App, {});
function create(/* VNode */node, /* HTMLElement */parent) {
node = normalize(node); // "str" -> {tag: "#", children: "..."}
let el;
if (TEXT_NODE === node.tag) {
el = document.createTextNode(node.children);
} else {
el = document.createElement(node.tag);
updateAttrs(el, node.attrs); // добавляем аттрибуты
createChildren(node, el); // создаеём детишек
}
node.el = el; // запоминаем DOM
(parent !== null) && parent.appendChild(el);
return node;
}
function updateAttrs(/* HTMLElement */el, attrs, oldAttrs) {
let name;
let value;
for (name in attrs) {
value = attrs[name];
#!+ if (oldAttrs == null || oldAttrs[name] !== value) {
#!+ if (CLASS_ATTR === name) { // className
el.className = value;
#!- }
#!+ else if (VALUE_ATTR === name) { // value
(el[name] !== value) && (el[name] = value);
#!- }
#!+ else if (/^on[A-Z]/.test(name)) { // события
el.addEventListener(name.substr(2).toLowerCase(), value, false);
#!- }
#!+ else { // все остальные
el.setAttribute(name, value);
#!- }
#!- }
}
// продолжение следует
.
// продолжение
if (oldAttrs != null) {
for (name in oldAttrs) {
value = oldAttrs[name];
if (attrs[name] == null && attrs[name] !== value) {
if (/^on[A-Z]/.test(name)) {
el.removeEventListener(name.substr(2).toLowerCase(), value, false);
} else {
el.removeAttribute(name);
}
}
}
}
}
function createChildren(/* VNode */node, /* HTMLElement */parent) {
const children = node.children;
const length = children.length;
#!+ if (length == null || /string|number|boolean/.test(typeof children)) {
#! node.children = [create(children, parent)];
} else {
#!+ for (let i = 0; i < length; i++) {
children[i] = create(children[i], parent);
#!- }
#!- }
}
<div id="root"></div>
<script>
const vnode = create({
#!+ tag: "div",
attrs: {class: "app"},
children: [
#! {tag: "h1", children: "Hi, %username%!"},
#! {tag: "button", attrs: {class: "button"}, children: "Enter"},
#!- ],
}, document.getElementById("root"));
</script>
function update(/* VNode */oldNode, /* VNode */newNode, /* HTMLElement */parent) {
const el = oldNode.el;
newNode = normalize(newNode);
#!+ if (newNode.tag === oldNode.tag) {
#! newNode.el = el;
#!+ if (TEXT_NODE === oldNode.tag) { // Текст
#!+ if (oldNode.children !== newNode.children) {
el.textContent = newNode.children;
#!- }
} else { // Внутренности
#!+ updateAttrs(el, newNode.attrs, oldNode.attrs);
#!- updateChildren(oldNode, newNode);
#!- }
} else {
#! newNode = create(newNode, null);
#! parent.replaceChild(newNode.el, el);
#!- }
return newNode;
}
function updateChildren(/* VNode */oldNode, /* VNode */newNode) {
const el = newNode.el;
const oldChildren = oldNode.children;
const oldLength = oldChildren.length;
let newChildren = newNode.children;
let newLength = newChildren.length;
#!+ if (newLength == null || /string|number|boolean/.test(typeof newChildren)) {
newLength = 1;
newNode.children = newChildren = [newChildren];
#!- }
#!+ for (let idx = 0; idx < newLength; idx++) {
const newChild = newChildren[idx];
if (idx < oldLength) { // обновляем узел
#! newChildren[idx] = update(oldChildren[idx], newChild, el);
} else { // создаём новый в конце
#! newChildren[idx] = create(newChild , el);
}
#!- }
#!+ for (let i = newLength; i < oldLength; i++) { // удаляем хвост
el.removeChild(oldChildren[i].el);
#!- }
}
function render(container, node) {
#! let vnode = container.vnode;
#!+ if (vnode) {
vnode = update(vnode, node, container);
} else {
vnode = create(node, container);
#!- }
#!+ container.vnode = vnode;
#!- return vnode;
}
let name;
const container = document.getElementById("root");
const redraw = () => render(container, createFragment());
#!+ function createFragment() {
return {
tag: "div",
attrs: {class: "app"},
children: [
#! {tag: "h1", children: `Hi, ${name || '%username%'}!`},
#!+ {tag: "button", attrs: {
class: "button",
name: **name ? "exit" : "enter"**,
onClick: handleClick,
#!- }, children: **name ? "Exit" : "Enter"**},
]
};
#!- }
#!+ function handleClick(evt) {
name = evt.currentTarget.name === "enter" && prompt("Name?");
#! redraw();
#!- }
#! redraw();