Skip to content

DOM virtual (vnode)

Um DOM virtual leve ao estilo do Snabbdom. Ele representa sua interface com objetos JavaScript comuns (os VNode), compara uma árvore velha com uma nova e aplica ao DOM real só as diferenças.

  • init() monta o reconciliador e devolve uma função patch. Os módulos que vêm junto (class / props / attrs / style / events) se registram sozinhos.
  • patch(oldVnode, newVnode) monta uma árvore (quando oldVnode é um elemento real do DOM) ou compara duas árvores de vnodes e atualiza o DOM ali mesmo.
  • h(sel, dataOrChildren?, children?) é o auxiliar de hyperscript que constrói os VNode.

Importar

js
import { init, h, classModule, propsModule, styleModule, eventListenersModule } from 'ranuts/vnode';

Nota: nesta implementação o init() não recebe argumentos: o conjunto de módulos é fixo e se registra por dentro, então não é preciso passar cada *Module para o init. Eles são exportados para consulta e inspeção.

Exemplo

Primeiros passos

js
import { init, h } from 'ranuts/vnode';

// init() devolve uma função `patch`.
// Os módulos que vêm junto (class, props, attrs, style, events) se registram sozinhos.
const patch = init();

const container = document.getElementById('app');

// Monta uma árvore de vnodes
let vnode = h('div#app.container', { style: { color: 'red' } }, [
  h('h1', 'Hello vnode'),
  h('button', { on: { click: () => console.log('clicked') } }, 'Click me'),
]);

// Primeiro desenho: passe um elemento real do DOM como vnode velho para montar dentro dele
patch(container, vnode);

// Depois: monte a árvore atualizada e aplique sobre ela o vnode anterior.
// Ao DOM chegam só as diferenças (texto, estilo, ouvintes).
const newVnode = h('div#app.container', { style: { color: 'green' } }, [
  h('h1', 'Hello again'),
  h('button', { on: { click: () => console.log('clicked') } }, 'Updated'),
]);

patch(vnode, newVnode);
vnode = newVnode; // guarde a última árvore para o próximo patch

Construir nós com h

js
// só a tag
h('div');

// tag + data
h('div', { class: { active: true } });

// tag + um único filho de texto
h('span', 'hello');

// tag + array de filhos
h('ul', [h('li', 'one'), h('li', 'two')]);

// tag + data + filhos
h('a', { attrs: { href: '/home' } }, 'Home');

// Seletores ao estilo do CSS definem o id e as classes
h('div#main.card.large', 'content'); // <div id="main" class="card large">content</div>

// O espaço de nomes do SVG entra sozinho quando o seletor começa com "svg"
h('svg', { attrs: { width: 100, height: 100 } }, [h('circle', { attrs: { cx: 50, cy: 50, r: 40 } })]);

API

init()

Cria o reconciliador e devolve uma função patch. Os módulos que vêm junto são registrados por dentro; ele não recebe argumentos.

Retorna

ValorDescriçãoTipo
patchMonta árvores de vnodes contra o DOM real, ou as compara(oldVnode: VNode | Element, vnode: VNode) => VNode

patch(oldVnode, vnode)

É o que o init() devolve. Na primeira chamada, passe um Element real do DOM como oldVnode para montar a árvore dentro dele. Nas seguintes, passe o VNode anterior para comparar e atualizar ali mesmo. Devolve o novo VNode, que você guarda como valor «velho» para a chamada seguinte.

Parâmetros

ParâmetroDescriçãoTipo
oldVnodeO vnode anterior, ou um elemento do DOM na primeira montagemVNode | Element
vnodeA nova árvore de vnodes a desenharVNode

h(sel, dataOrChildren?, children?)

Auxiliar de hyperscript que constrói um VNode. Ele é sobrecarregado:

AssinaturaDescrição
h(sel)Elemento a partir de um seletor, e mais nada
h(sel, data)Elemento com VNodeData (o data pode ser null)
h(sel, children)Elemento com filhos: um texto ou número, um VNode, ou um array
h(sel, data, children)Elemento com dados e filhos

Parâmetros

ParâmetroDescriçãoTipo
selSeletor ao estilo do CSS: tag, tag#id, tag.class, ou combinados (div#id.a.b). Com svg… o espaço de nomes do SVG entra sozinhostring
dataDados do nó: class / props / attrs / style / ouvintes / key / hook. Pode ser nullVNodeData | null
childrenUm texto ou número (que vira um nó de texto), um único VNode, ou um array delesVNodeChildren

Campos de VNodeData

CampoDescriçãoTipoQuem aplica
propsPropriedades do DOM atribuídas com elm[key] = valueRecord<string, any>propsModule
attrsAtributos HTML postos com setAttribute (true e false põem e tiram o atributo)Record<string, string | number | boolean>attributesModule
classClasses condicionais: uma correspondência name → booleanRecord<string, boolean>classModule
styleEstilos em linha: uma correspondência name → value (chaves --var viram variáveis CSS)Record<string, any>styleModule
onOuvintes de evento: event → handler (ou um array de manipuladores)Record<string, Function | Function[]>eventListenersModule
keyIdentidade estável que o algoritmo de comparação usa para casar e reordenar os filhosstring | number(o núcleo da comparação)
nsURI do espaço de nomes (posta sozinha nas subárvores SVG)string(o núcleo da comparação)
hookGanchos de ciclo de vida por vnode (Hooks)Hooks(só como tipo: veja a nota)

Nota: o hook e o tipo Hooks fazem parte da superfície pública de tipos. Esta implementação enxuta move o DOM pelo ciclo de vida dos módulos (create / update / destroy); o laço atual do patch não chama os data.hook de cada vnode.

Módulos

Cada módulo cuida de uma fatia do VNodeData. O init() registra todos eles; além disso, são exportados um a um.

ExportaçãoCuida deDescrição
classModuledata.classPõe e tira classes a partir de uma correspondência name → boolean
propsModuledata.propsAtribui propriedades do DOM diretamente (elm[key] = value)
attributesModuledata.attrsPõe e tira atributos HTML com setAttribute (inclusive xml e xlink)
styleModuledata.styleAplica estilos em linha e propriedades personalizadas do CSS
eventListenersModuledata.onPrende e solta ouvintes de evento
modulesO objeto de registro padrão, que liga cada nome de módulo ao seu módulo

Exportações de nível mais baixo

ExportaçãoTipoDescrição
vnode(sel, data, children, text, elm) => VNodeFábrica de VNode de baixo nível que o h usa por dentro. No código da aplicação, prefira o h.
addNS(data, children, sel) => voidAplica o espaço de nomes do SVG a uma subárvore inteira. O h a chama sozinho para seletores svg….
htmlDomApiDOMAPIO adaptador de DOM de navegador padrão que o patch usa por dentro (criar, inserir, remover, nós de texto etc.).
is{ array, isStr, primitive, isVnode }Pequenos auxiliares de verificação de tipo usados por todo o interior do vnode.
Chainclass ChainUm construtor de DOM imperativo e encadeável (setAttribute, append, setTextContent, …). Nada tem a ver com a comparação de vnodes.
create(tagName, options?) => ChainFábrica de conveniência que devolve um Chain novo.

Tipos

TipoFormato e sentido
VNode{ sel, data, children, elm, text, key, listener? }: um nó virtual
VNodeData{ props?, attrs?, class?, style?, on?, key?, ns?, hook? }: veja os campos acima
VNodesVNode[]
VNodeChildElementVNode | string | number
VNodeChildrenVNodeChildElement | VNodeChildElement[]
ArrayOrElement<T>T | T[]
Keystring | number
Hooks{ pre?, init?, create?, insert?, prepatch?, update?, postpatch?, destroy?, remove?, post? }
DOMAPIInterface que descreve as operações de DOM que o patch usa (veja htmlDomApi)
FragmentExtensão de DocumentFragment usada no trato com fragmentos
ModulesRecord<string, Record<string, ModuleHook>>: o formato do registro de módulos
ModuleHookUma única chamada do ciclo de vida de um módulo

Notas

  1. Só navegador. O ranuts/vnode mexe com document e com as APIs do DOM; importe-o em código de navegador, não no Node.
  2. Guarde o último vnode. O patch devolve o novo VNode. Guarde-o e passe-o como oldVnode na atualização seguinte, para que as diferenças sejam calculadas contra a árvore vigente.
  3. Num VNode, text e children se excluem: um nó é ou um nó de texto, ou um elemento com filhos.
  4. Use key nas listas. Ao desenhar listas que mudam, dê aos irmãos valores de key estáveis, para que a comparação possa casar e reordenar os nós em vez de refazê-los.

Publicado sob a licença MIT.