Не стоит воспринимать этот материал как руководство к действию, это только базовые принципы работы с 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();