O IVI é uma biblioteca de interface do usuário declarativa incorporável e incorporável.
f(state) => UI import { createRoot , update , component , useState , html } from "ivi" ;
const Example = component ( ( c ) => {
const [ count , setCount ] = useState ( c , 0 ) ;
const inc = ( ) => { setCount ( count ( ) + 1 ) ; } ;
return ( ) => html `
< div class =" app " >
< div > ${ count ( ) } </ div >
< button @click = ${ inc } > Increment </ button >
</ div >
` ;
} ) ;
update (
createRoot ( document . body ) ,
Example ( ) ,
) ;O tamanho do exemplo pré -compilado acima é de apenas 2,7kb (Minified+brotli). Inclui tempo de execução inteiro para renderização declarativa da interface do usuário. Os modelos pré-compilados são otimizados para o tamanho do código e o desempenho de partida a frio.
createRoot(parentElement, nextNode)dirtyCheck(root, forceUpdate)update(root, v, forceUpdate)unmount(root, detach)defineRoot(onInvalidate)component(factory, areEqual)getProps(component)invalidate(component)useUnmount(component, hook)useMemo(areEqual, fn)useState(component, value)useReducer(component, value, reducer)useEffect(component, effect, areEqual)useLayoutEffect(component, effect, areEqual)useIdleEffect(component, effect, areEqual)List(entries, getKey, render)context()eventDispatcher(eventType, options)findDOMNode(node)containsDOMElement(node, element)hasDOMElement(node, element)preventUpdates(a, b)strictEq(a, b)shallowEq(a, b)shallowEqArray(a, b)Os modelos IVI funcionarão sem nenhuma pré -compilação, mas é altamente recomendável usar a pré -compilação para melhorar o desempenho e reduzir o tamanho do código.
O pacote "@ivi/vite-plugin" fornece plug-in de vite.
// vite.config.mjs
import { defineConfig } from "vite" ;
import { ivi } from "@ivi/vite-plugin" ;
export default defineConfig ( {
plugins : [ ivi ( ) ] ,
} ) ; O pacote "@ivi/rollup-plugin" fornece plug-in de rollup.
// rollup.config.mjs
import { ivi } from "@ivi/rollup-plugin" ;
export default {
input : "src/main.js" ,
output : {
file : "bundle.js" ,
} ,
plugins : [ ivi ( ) ]
} ; A linguagem do modelo IVI possui uma sintaxe do tipo HTML com sintaxe adicional para propriedades DOM, eventos e remoção de espaço em branco.
html cria um modelo com nós HTMLELEMENT.svg cria um modelo com nós SvgElement. import { html } from "ivi" ;
const Example = component ( ( c ) => {
// ...
return ( ) => html `
< div class =" app " >
< div > ${ count ( ) } </ div >
< button @click = ${ inc } > Increment </ button >
</ div >
` ;
} ) ;Os modelos podem ter vários nós raiz.
html `
< div > </ div >
${ expr }
text
< div > </ div >
` Os elementos sem filhos podem ser fechados com a /> .
html `
< div
class =" a "
/>
` ; < div >
< p > </ p >
ab
< p > </ p >
</ div > < div > < p > </ p > ab < p > </ p > </ div > < div > < span > a b </ span > </ div > < div > < span > a b </ span > </ div > < div >
ab
cd
</ div > < div > ab cd </ div >v impede de remover todos os espaços de brancos em torno das newlines: < div >
< b > 1 </ b >
v item left
< div > < div > < b > 1 </ b > item left </ div >Nos modelos IVI, você pode incluir conteúdo dinâmico chamado expressões. Uma expressão é apenas uma peça do código JavaScript que é avaliado quando o modelo é renderizado. Qualquer que seja o valor que uma expressão produza naquele momento será incluído no modelo final renderizado.
html `
< div attr = ${ attributeValueExpr } >
${ childExpr }
</ div > ` ;O modelo IVI Modelo suporta sintaxe adicional para trabalhar com propriedades, eventos, etc.
<div name="value" /> - atributo estático.<div name /> - atributo estático.<div name=${expr} /> - Dynamic Attribute element.setAttribute(name, expr) .<div .name=${expr} /> - element[name] = expr .<div *name=${expr} /> - element[name] = expr , difere em um valor dom.<div ~name="value" /> - estilo estático <div style="name:value;"> .<div ~name=${expr} /> - element.style.setProperty(name, expr) .<div @name=${expr} /> - Event element.addEventListener(name, expr) .<div ${directive} /> - Diretiva do elemento do lado do cliente directive(element) .<div .textContent=${expr} /> - conteúdo de texto. <div name="value" /> - atributo estático com um valor <div name="value"> .<div name /> - atributo estático sem um valor <div name> .<div name=${expr} /> - Dynamic Attribute element.setAttribute(name, expr) . Os atributos DOM são atribuídos com Element.setAttribute(..) .
Quando o atributo dinâmico possui um valor undefined , null ou false , ele será removido do elemento DOM com Element.removeAttribute(..) método.
<div .name=${expr} /> - element[name] = expr .<div *name=${expr} /> - element[name] = expr , difere em um valor dom. As propriedades são atribuídas com um Element.name = value .
Diffing com um valor DOM é útil nos casos de uso quando usamos valores <input> para evitar desencadear eventos input desnecessários.
<div ~name="value" /> - estilo estático <div style="value"> .<div ~name=${expr} /> - element.style.setProperty(name, expr) . Os estilos estáticos são automaticamente mesclados com o atributo :style="value" .
Os estilos dinâmicos são atribuídos com um método CSSStyleDeclaration.setProperty(..) .
Quando o estilo possui um valor undefined , null ou false , ele será removido com o método CSSStyleDeclaration.removeProperty(..) .
<div @name=${expr} /> - Event element.addEventListener(name, expr) . Os eventos são atribuídos com um método EventTarget.addEventListener(..) .
Quando o evento tiver um valor undefined , null ou false , ele será removido com o método EventTarget.removeEventListener(..) .
<div .textContent=${expr} /> - Texto conteúdo element.textContent = expr . A propriedade de conteúdo de texto pode ser usada como uma otimização que reduz ligeiramente o consumo de memória para elementos com uma criança de texto. Ele criará um nó de texto com uma propriedade Node.textContent e não terá nenhum nós com estado associado a um nó de texto.
O valor do conteúdo de texto deve ter um tipo undefined , null , false , string ou um number .
<div ${directive} /> - directive(element) .A diretiva é uma função que é invocada cada vez que o modelo é atualizado e recebe um elemento DOM associado a uma diretiva:
type ElementDirective = < E extends Element > (
element : E ,
) => void ;A função diretiva é invocada apenas quando o modelo é criado com uma função diferente; portanto, se vamos reutilizar a mesma função, ele pode ser usado como um elemento DOM criado por retorno de chamada:
const Example = component ( ( c ) => {
const onCreated = ( innerElement ) => {
// ..
} ;
return ( ) => html `
< div >
< div class =" Inner " ${ onCreated } />
</ div >
` ;
} ) ;As diretrizes podem ser usadas não apenas como um simples retorno de chamada criado pela DOM, mas também como diretrizes com estado. Por exemplo
function createStatefulDirective ( ) {
// Internal state that stores previous value.
let prev ;
// Returns a factory that creates directive functions.
return ( next ) => ( element ) => {
// Check if previous value has been changed.
if ( prev !== next ) {
prev = next ;
// Updates textContent only when input value is changed.
element . textContent = next ;
}
} ;
}
const Example = component ( ( c ) => {
const directive = createStatefulDirective ( ) ;
return ( i ) => htm `
<div ${ directive ( i ) } />
` ;
} ) ;Você pode usar expressões regulares de JavaScript em seus modelos, o que significa que você pode usar quaisquer construções de fluxo de controle JavaScript, como operadores condicionais, chamadas de função e instruções se ou alternar para gerar conteúdo dinâmico com base nas condições de tempo de execução.
Isso significa que você pode criar modelos com lógica complexa que condicionalmente renderiza conteúdo diferente com base no que está acontecendo em seu aplicativo. Você pode aninhar expressões de modelos dentro um do outro para criar modelos mais complexos e pode armazenar os resultados de modelos em variáveis para usá -las posteriormente no seu código.
const Example = component ( ( c ) => {
// ...
return ( show ) => html `
< div >
${ show && html ` < span > Show </ span > ` }
</ div >
` ;
} ) ;Se uma expressão for usada na posição criança de um elemento HTML e retornar uma matriz, o IVI renderizará todos os itens nessa matriz como nós separados.
const Example = ( ) => html `
< div >
${ [
"Text Node 1" ,
"Text Node 2" ,
] }
</ div >
` ;O IVI permite que os componentes retornem matrizes de elementos como seus nós raiz. Isso significa que um componente pode retornar vários elementos de nível superior em vez de apenas um.
Por exemplo, um componente pode retornar uma matriz de elementos <li> que compõem uma lista. Quando esse componente for renderizado, o IVI tratará a matriz de elementos <li> como um conjunto de elementos de nível superior, assim como faria com um único elemento raiz.
Esse recurso fornece mais flexibilidade ao criar componentes complexos da interface do usuário, pois permite criar componentes que geram um número dinâmico de elementos de nível superior, dependendo de sua entrada.
const Example = component ( ( c ) => {
return ( entries ) => entries . map ( ( e ) => html `
< li > ${ e } </ li >
` ) ;
) ;
// Example([1, 2, 3])Quando as matrizes são atualizadas, os nós de árvore sem estado são mapeados em seus nós com estado de estado por sua posição na matriz.
Quando a matriz contém uma expressão condicional que retorna um valor de "orifício" ( null , undefined ou false ), o buraco ocupará um slot em uma árvore de estado, para que todos os nós sejam correcidos mapeados em seus nós de estado.
[
conditional ? "text" : null ,
StatefulComponent ( ) ,
] No exemplo acima, quando a expressão conditional vai de um texto para um "buraco" e vice -versa, StatefulComponent preservará seu estado interno.
Quando a matriz crescer ou diminuir de tamanho, os nós com estado serão criados ou removidos no final de uma matriz.
No IVI, você pode renderizar listas de itens usando a função List() que percorre uma matriz de dados e retorna uma lista de elementos. No entanto, quando a lista é atualizada, é importante mapear corretamente itens renderizados em suas visões com estado. Isso significa que, se um item for renderizado como um componente que possui estado interno que pode mudar como resultado de ações do usuário ou eventos externos, ele deve ser mapeado na mesma instância do componente.
Para renderizar listas dinâmicas, o IVI fornece a função List() .
function List < E , K > (
// Input Entries.
entries : E [ ] ,
// Function that retrieves unique key from an entry.
getKey : ( entry : E , index : number ) => K ,
// Function that renders an entry.
render : ( entry : E ) => VAny ,
) : VList ;Ele cria uma lista dinâmica com uma matriz de chaves que identificam de maneira exclusiva cada item da lista. Quando a lista é atualizada, o IVI usa teclas para mapear itens em seus nós com estado.
É importante observar que, ao renderizar uma lista dinâmica, você sempre deve usar um identificador exclusivo como chave. Isso ajuda o IVI a identificar cada elemento em uma lista e evitar erros de renderização. Se você usar um índice ou um valor aleatório como chave, o IVI pode não ser capaz de identificar elementos corretos em uma lista, o que pode causar erros.
interface DataEntry {
key : number ;
text : string ;
}
const getEntryKey = ( entry : DataEntry ) => entry . key ;
const EntryView = ( entry : DataEntry ) => (
html ` < li > ${ entry . text } </ li > `
) ;
const ListView = ( data : DataEntry [ ] ) => html `
< ul > ${ List ( data , getEntryKey , EntryView ) } </ ul >
` ; O IVI está usando um algoritmo ideal para listas dinâmicas que usam o número mínimo de operações Node.insertBefore() para reorganizar os nós DOM.
Reduzir Node.insertBefore() Operações é importante não apenas porque invalida o estado interno da DOM, mas também porque cada vez que um dos nós DOM anexados ao documento é movido, ele pode produzir uma notificação de mutationObserver. E muitas extensões populares estão usando observadores de mutação para observar a subárvore inteira de documentos; portanto, cada operação insertBefore pode se tornar bastante cara quando é usada fora das caixas de areia de benchmarking.
Os componentes podem ser de estado ou sem estado. Os componentes estabelecidos são usados quando você precisa gerenciar que as mudanças ao longo do tempo, como entrada do usuário, solicitações de rede ou animações.
Os componentes com estado são declarados com a função component() . Ele cria uma função de fábrica que produz nós de componentes.
// `component()` function creates a factory function for component
// nodes of this type.
const Example = component ( ( c ) => {
// When component state is initialized, it should return a render
// function.
return ( props ) => (
html ` < div > ${ props . value } </ div > `
) ;
} ) ;
update (
document . body ,
Example ( { value : "Hello World" } ) ,
) ;Os componentes com estado estão usando o fechamento de JavaScript para armazenar o estado interno.
const Example = component ( ( c ) => {
// Internal state.
let _counter = 0 ;
// Event handler.
const increment = ( ) => {
// Mutate internal state.
_counter ++ ;
// Invalidate component and schedule an update.
invalidate ( c ) ;
} ;
// Render function.
return ( ) => html `
< div >
< p > Count: ${ _counter } </ p >
< button @click = ${ increment } > Increment </ button >
</ div >
` ;
} ) ; Quando o estado interno é mutado, ele não aciona as atualizações de componentes automaticamente e deve ser invalidado manualmente com a função invalidate() .
Existem APIs de alto nível como useState() ou useReducer() que usam a função invalidate() de baixo nível para invalidar automaticamente componentes quando o estado interno é mutado.
const Example = component ( ( c ) => {
// Internal state.
const [ counter , setCounter ] = useState ( c , 0 ) ;
const increment = ( ) => {
// Automatically invalidates component when counter value is mutated.
setCounter ( counter ( ) + 1 ) ;
} ;
// Render function.
return ( ) => (
html `
< div >
< p > Count: ${ counter ( ) } </ p >
< button @click = ${ increment } > Increment </ button >
</ div > `
) ;
} ) ;Componentes sem estado no IVI são apenas funções básicas de JavaScript. Eles são mais rápidos e mais leves do que os componentes com estado, o que os torna uma boa escolha para componentes simples e reutilizáveis que não têm nenhum estado interno.
const Button = ( text , onClick ) => html `
< button @click = ${ onClick } > ${ text } </ button >
` ; type SNode = Opaque ;
type Root < State > = Opaque < State > ;
type Component < Props > = Opaque < Props > ; type VAny =
| null // Hole
| undefined // Hole
| false // Hole
| string // Text
| number // Text
| VRoot // Root
| VTemplate // Template
| VComponent // Component
| VContext // Context Provider
| VList // Dynamic List with track by key algo
| VAny [ ] // Dynamic List with track by index algo
;
type VRoot = Opaque ;
type VTemplate = Opaque ;
type VComponent = Opaque ;
type VContext = Opaque ;
type VList = Opaque ;Um nó raiz é o nó mais alto em uma árvore de estado, da qual todos os outros nós são renderizados. Representa um ponto de entrada para o algoritmo de renderização IVI e armazena uma posição na árvore Dom.
createRoot() createRoot cria um nó raiz que usa a fila Microtosk para agendar atualizações.
function createRoot (
parentElement : Element ,
nextNode : Node | null = null ,
) : Root ;parentElement - Elemento DOM do pai.nextNode - Nó no próximo DOM. dirtyCheck() dirtyCheck executa o algoritmo de verificação suja em uma subárvore raiz e atualiza todos os componentes sujos.
function dirtyCheck (
root : Root ,
forceUpdate : boolean = false ,
) : void ;root - nó raiz.forceUpdate - Force todos os componentes a serem atualizados, mesmo quando estão usando dicas de otimização para reduzir as atualizações. update() update atualiza uma subárvore root com uma nova representação.
function update (
root : Root ,
v : VAny ,
forceUpdate : boolean = false ,
) : void ;root - nó raiz.v - Nova representação.forceUpdate - Force todos os componentes a serem atualizados, mesmo quando estão usando dicas de otimização para reduzir as atualizações. unmount() unmount desmontar uma subárvore raiz do DOM e desencadeia ganchos desmontados em componentes.
function unmount (
root : Root ,
detach : boolean ,
) : void ;root - nó raiz.detach - destacar os nós mais tops dom das subáridas DOM. defineRoot() defineRoot cria uma fábrica de nó raiz que usa um gancho OnRootInvalidated personalizado.
function defineRoot (
onInvalidate : ( root : Root < undefined > ) => void ,
) : ( parentElement : Element , nextNode : Node | null ) => Root < undefined > ;
function defineRoot < S > (
onInvalidate : ( root : Root < S > , state : S ) => void ,
) : ( parentElement : Element , nextNode : Node | null , state : S ) => Root < S > ;onInvalidate - OnRootInvalidated que recebe um nó raiz e um estado personalizado associado a esse nó raiz.component() component cria uma fábrica que produz nós de componentes.
function component (
factory : ( c : Component ) => ( ) => VComponent < undefined > ,
areEqual ?: ( ) => boolean
) : ( ) => VComponent < undefined > ;
function component < P > (
factory : ( c : Component ) => ( props : P ) => VAny ,
areEqual ?: ( prev : P , next : P ) => boolean
) : ( props : P ) => VComponent < P > ;factory - Função que produz funções de renderização com componentes com estado.areEqual - que verifica as propriedades de entrada para alterações e é usada como uma dica de otimização para reduzir as atualizações desnecessárias quando as propriedades não foram alteradas. Quando a subárvore raiz é atualizada com a opção forceUpdate , a dica areEqual é ignorada e todos os componentes são atualizados.
getProps() getProps recebe adereços de componentes atuais da instância do componente.
function getProps = < P > ( component : Component < P > ) : P ;component - instância do componente. invalidate() invalidate invalida o componente e agenda uma atualização.
function invalidate ( component : Component ) : void ;component - instância do componente. useUnmount()Adiciona um gancho de desmontagem.
function useUnmount (
component : Component ,
hook : ( ) => void ,
) : void ;component - instância do componente.hook - gancho desmontado.useMemo() useMemo cria uma função memorada.
function useMemo < T , U > (
areEqual : ( prev : T , next : T ) => boolean ,
fn : ( props : T ) => U ,
) : ( props : T ) => U ;areEqual - verifica as propriedades de entrada para alterações para evitar recomputações.fn - função de memórias. useState() useState cria um estado de componente reativo.
function useState < S > (
component : Component ,
state : S ,
) : [
get : ( ) => S ,
set : ( s : S ) => void ,
] ;component - instância do componente.state - estado inicial.Retorna as funções estaduais de getter e estadual.
useReducer() useReducer cria um redutor de estado de componente reativo.
type Dispatch < A > = ( action : A ) => void ;
function useReducer < S , A > (
component : Component ,
state : S ,
reducer : ( state : S , action : A ) => S ,
) : [
get : ( ) => S ,
dispatch : Dispatch < A > ,
] ;component - instância do componente.state - estado inicial.reducer - Função do redutor de estado.Retorna as funções estaduais de getter e despachante de ação.
Os efeitos colaterais permitem especificar como seus componentes devem se comportar com sistemas externos, como chamadas de API imperativas, manipulações do timer ou interações diretas DOM.
Você pode pensar nisso como uma combinação de ganchos de mount , update e unmount .
useEffect() useEffect cria um efeito colateral que é executado imediatamente após o término do nó raiz.
function useEffect (
component : Component ,
effect : ( ) => ( ( ) => void ) | void ,
) : ( ) => void ;
function useEffect < P > (
component : Component ,
effect : ( props : P ) => ( ( ) => void ) | void ,
areEqual ?: ( prev : P , next : P ) => boolean
) : ( props : P ) => void ;component - instância do componente.effect - Efeito gancho.areEqual - que verifica as propriedades de entrada para alterações e é usada para controlar quando um efeito deve ser atualizado.Retorna uma função de efeito colateral que deve ser invocada em uma função de renderização.
useLayoutEffect() useLayoutEffect cria um efeito colateral que é executado antes do quadro de animação.
function useLayoutEffect (
component : Component ,
effect : ( ) => ( ( ) => void ) | void ,
) : ( ) => void ;
function useLayoutEffect < P > (
component : Component ,
effect : ( props : P ) => ( ( ) => void ) | void ,
areEqual ?: ( prev : P , next : P ) => boolean
) : ( props : P ) => void ;component - instância do componente.effect - Efeito gancho.areEqual - que verifica as propriedades de entrada para alterações e é usada para controlar quando um efeito deve ser atualizado.Retorna uma função de efeito colateral que deve ser invocada em uma função de renderização.
useIdleEffect() useIdleEffect cria um efeito colateral que é executado quando o navegador está ocioso.
function useIdleEffect (
ccomponent : Component ,
effect : ( ) => ( ( ) => void ) | void ,
) : ( ) => void ;
function useIdleEffect < P > (
ccomponent : Component ,
effect : ( props : P ) => ( ( ) => void ) | void ,
areEqual ?: ( prev : P , next : P ) => boolean
) : ( props : P ) => void ;component - instância do componente.effect - Efeito gancho.areEqual - que verifica as propriedades de entrada para alterações e é usada para controlar quando um efeito deve ser atualizado.Retorna uma função de efeito colateral que deve ser invocada em uma função de renderização.
List() List cria listas dinâmicas.
function List < E , K > (
entries : E [ ] ,
getKey : ( entry : E , index : number ) => K ,
render : ( entry : E ) => VAny ,
) : VList ;entries - dados de entrada.getKey - Função que deve retornar uma chave exclusiva para cada entrada de dados.render - função que renderiza uma entrada.context() context cria funções de contexto e provedor de contexto.
function context = < T > ( ) : [
get : ( component : Component ) => T | undefined ,
provider : ( value : T , children : VAny ) => VContext < T > ,
] Retorna uma função get que encontra o valor de contexto mais próximo e uma função provider que cria nós de contexto.
// Creates a getter and provider functions.
const [ getContextValue , contextValueProvider ] = context ( ) ;
const Example = component ( ( c ) => {
return ( ) => html `
< h1 > Hello ${ getContextValue ( c ) } </ h1 >
` ;
} ) ;
update (
createRoot ( document . body ) ,
contextValueProvider (
"World" ,
Example ( ) ,
) ,
) ; ElementDirective é uma escotilha de fuga que permite estender o algoritmo de renderização de IVI.
type ElementDirective = < E extends Element > (
element : E ,
) => void ;eventDispatcher() eventDispatcher cria um despachante de eventos que encontra o nó do filho mais próximo e emite um CustomEvent com o método EventTarget.dispatchEvent() .
interface DispatchEventOptions {
// Option indicating whether the event bubbles. The default
// is `true`.
bubbles ?: boolean ;
// Option indicating whether the event can be cancelled. The
// default is `false`.
cancelable ?: boolean ;
// Option indicating whether the event will trigger listeners
// outside of a shadow root. The default is `false`.
composed ?: boolean ;
}
type EventDispatcher = {
( component : Component ) : boolean ;
< T > ( component : Component , value : T ) : boolean ;
} ;
function eventDispatcher = < T > (
eventType : string ,
options ?: DispatchEventOptions ,
) : EventDispatcher ;eventType - Tipo de evento.options - Opções de evento que serão usadas quando o evento for despachado.O despachante de eventos chama os manipuladores de eventos de maneira síncrona. Todos os manipuladores de eventos são chamados antes do retorno do despachante de eventos.
findDOMNode() findDOMNode encontra o filho do nó DOM mais próximo que pertence a uma subárvore de nós com estado.
function findDOMNode < T extends Node | Text > (
node : SNode | null ,
) : T | null ;node - Nó com estado. containsDOMElement() containsDOMElement verifica se um nó com estado contém elementos DOM em sua subárvore.
function containsDOMElement (
node : SNode ,
element : Element ,
) : boolean ;node - Nó com estado.element - elemento dom. hasDOMElement() hasDOMElement verifica se um nó com estado tem um elemento DOM como filho.
function hasDOMElement (
node : SNode ,
child : Element ,
) : boolean ;node - Nó com estado.child - DOM elemento.preventUpdates() preventUpdates é uma função Noop que sempre retorna o valor true .
function preventUpdates < T > ( a : T , b : T ) : true ; strictEq() strictEq verifica os valores da igualdade com o operador estrito da igualdade === .
function strictEq < T > ( a : T , b : T ) : boolean ; shallowEq() shallowEq verifica objetos com algoritmo de igualdade superficial e usa o operador estrito da igualdade para verificar os valores individuais quanto à igualdade.
function shallowEq < T extends object > ( a : T , b : T ) : boolean ; shallowEqArray() shallowEqArray verifica matrizes com algoritmo de igualdade superficial e usa o operador de igualdade rigoroso para verificar os valores individuais quanto à igualdade.
function shallowEqArray < T > ( a : T [ ] , b : T [ ] ) : boolean ; const Example = component ( ( ) => {
const _onTouchDown = ( ev ) => { } ;
const addPassiveTouchDown = ( element ) => {
element . addEventListener (
"touchdown" ,
_onTouchDown ,
{ passive : true } ,
) ;
} ;
return ( ) => html `
< div ${ addPassiveTouchDown } > </ div >
` ;
} ) ; const useDynamicArg = ( ) => {
let prevKey ;
let prevValue ;
return ( key , value ) => ( element ) => {
if ( prevKey !== key ) {
if ( prevKey ) {
element . removeAttribute ( prevKey ) ;
}
element . setAttribute ( key , value ) ;
} else if ( prevValue !== value ) {
element . setAttribute ( key , value ) ;
}
} ;
} ;
const Example = component ( ( ) => {
const arg = useDynamicArg ( ) ;
return ( [ key , value ] ) => html `
< div ${ arg ( key , value ) } > </ div >
` ;
} ) ; import { createRoot , update , component , findDOMNode , useEffect , html } from "ivi" ;
import { EditorView , basicSetup } from "codemirror" ;
import { javascript } from "@codemirror/lang-javascript" ;
const CodeMirror = component ( ( c ) => {
let _editor ;
useEffect ( c , ( ) => {
_editor = new EditorView ( {
extensions : [ basicSetup , javascript ( ) ] ,
// findDOMNode finds the closest child DOM node.
parent : findDOMNode ( c ) ,
} ) ;
// Reset function will be invoked when component is unmounted.
return ( ) => {
_editor . destroy ( ) ;
} ;
} ) ( ) ;
// ^ When effect doesn't have any dependencies, it can be executed just
// once in the outer scope. Effect will run when its DOM tree is mounted.
return ( ) => html `
< div class =" CodeMirror " > </ div >
` ;
} ) ;
update (
createRoot ( document . body ) ,
CodeMirror ( ) ,
) ; O algoritmo de invalidação do componente é implementado marcando o componente como sujo e marcando todos os seus nós pais com uma bandeira que eles têm uma subárvore suja. Quando o algoritmo de marcação atinge um nó raiz, ele chama o gancho OnRootInvalidated() que pode ser usado para implementar um agendador personalizado.
Dirty .DirtySubtree .DirtySubtree , o gancho OnRootInvalidated() invocado.Dirty , os pais já marcados com a bandeira DirtySubtree .Quando o Scheduler decide atualizar um nó raiz com uma subárvore suja, ele inicia um algoritmo de corrente suja. Esse algoritmo fica de cima para baixo em uma ordem da direita para a esquerda, visitando todos os nós com um sinalizador de subárvore sujo até atingir um componente sujo e atualizá-lo.
DirtySubtree , começa a verificar seus filhos.Dirty , aciona uma atualização.Dirty , aciona uma atualização. Uma das razões pelas quais a biblioteca principal é tão pequena é porque o algoritmo de atualização é implementado no pedido RTL. O algoritmo que executa atualizações no pedido da RTL simplifica muitos problemas complexos com as atualizações DOM. O principal problema com as atualizações DOM é que, quando começamos a atualizar uma estrutura de árvore Dom, precisamos ter uma referência a um pai e um próximo nó DOM, para que possamos usar parent.insertBefore(newNode, nextNode) . Na maioria dos casos, é fácil recuperar um próximo nó DOM, mas há casos de borda como quando temos duas expressões condicionais adjacentes e um de seus estados é que ele remove completamente um nó DOM da árvore, ou dois componentes adjacentes com condicionais em suas raízes etc.
A maioria das bibliotecas está lidando com esses casos de borda, introduzindo nós de DOM do marcador (comentário ou um nó de texto vazio). Por exemplo, para implementar expressões condicionais, podemos adicionar um nó de texto vazio quando o condicional não renderiza nenhum nó DOM e, quando o condicional entra em um estado quando precisar adicionar um nó DOM, ele usará um nó marcador como uma referência do Nó DOM. O algoritmo de atualização do RTL no IVI não usa nenhum nós de marcadores.
O algoritmo RTL usado no IVI também facilita muito a implementação de deslocamentos dos nó sem introduzir caminhos de código adicionais, fragmentos e praticamente tudo o que envolve a atualização de uma estrutura DOM.
Cada site de chamada que cria um modelo tem identidade única, portanto, mesmo modelos idênticos criados a partir de diferentes sites de chamada não poderão diferenciar um contra o outro.
function TemplateUniqueIdentity ( condition , text ) {
return ( condition )
? html `div ${ text } `
: html `div ${ text } ` ;
} No exemplo acima, quando condition é alterada, em vez de atualizar o nó de texto, o algoritmo de atualização substituirá todo o elemento div por um novo.
Existem alguns casos de uso que exigem muitas leituras frequentes de uma variável reativa. E sempre que essa variável muda, afeta muitos nós da interface do usuário, como alternar entre temas claros/escuros.
Em vez de criar muitas assinaturas para essas variáveis, é recomendável usar valores simples de JavaScript e render -render a subárvore inteira com dirtyCheck(root, true) quando esses valores são alterados.
const root = createRoot ( document . getElementById ( "app" ) ) ;
let theme = "Light" ;
function setTheme ( t ) {
if ( theme !== t ) {
theme = t ;
dirtyCheck ( root , true ) ;
}
}
const App = component ( ( c ) => {
const toggleTheme = ( ) => {
setTheme ( ( theme === "Light" ) ? "Dark" : "Light" ) ;
} ;
return ( ) => html `
div
div = ${ theme }
button @click= ${ toggleTheme } 'Toggle Theme'
` ;
} ) ;
update ( root , App ( ) ) ; A clonagem de modelos é uma otimização usada para clonar modelos HTML com um método Node.cloneNode() .
Por padrão, a clonagem de modelo é ativada para todos os modelos. Mas às vezes seria um desperdício criar um modelo para clonagem e instanciar a partir dele quando esse modelo for renderizado apenas uma vez.
Para desativar a clonagem, o modelo deve ter um comentário líder /* preventClone */ . Por exemplo
const Example = ( ) => /* preventClone */ html `
< div class =" Title " > ${ text } </ div >
` ; Modelos com apenas um elemento que não possuem propriedades estáticas serão criadas com document.createElement() .
html ` < div attr = ${ 0 } > ${ 1 } </ div > ` ;Por padrão, os manipuladores de eventos (expressões de função de seta) são automaticamente içados no escopo mais externo.
const Example = component ( ( c ) => {
const [ count , setCount ] = useState ( c , 0 ) ;
return ( ) => html `
< div @click = ${ ( ) => { setCount ( count ( ) + 1 ) ; } } > ${ count ( ) } </ div >
` ;
} ) ;Após a elevação do manipulador de eventos, ele será transformado em:
const Example = component ( ( c ) => {
const [ count , setCount ] = useState ( c , 0 ) ;
const __ivi_hoist_1 = ( ) => { setCount ( count ( ) + 1 ) ; } ;
return ( ) => html `
< div @click = ${ __ivi_hoist_1 } > ${ count ( ) } </ div >
` ;
} ) ; Para desativar os manipuladores de eventos que içam, o modelo deve ter um comentário líder /* preventHoist */ . Por exemplo
const Example = component ( ( c ) => {
const [ count , setCount ] = useState ( c , 0 ) ;
return ( ) => /* preventHoist */ html `
< div @click = ${ ( ) => { setCount ( count ( ) + 1 ) ; } } > ${ count ( ) } </ div >
` ;
} ) ; Várias anotações podem ser declaradas separando -as com | Operador, por exemplo /* preventClone | preventHoist */
Para obter uma estimativa aproximada do uso da memória, é importante entender as estruturas de dados internas.
Na descrição abaixo, calcularemos o uso da memória em um mecanismo baseado em cromo com compactação de ponteiro em V8.
A árvore da interface do usuário é implementada com um SNode de árvore com estado e uma árvore sem estado imutável VAny .
Árvore sem estado tem uma estrutura de dados simples:
// 20 bytes
interface VNode < D extends VDescriptor , P > {
// Descriptors are reused for all VNodes with the same type and its memory
// usage can be ignored during estimation.
readonly d : D ;
// Prop value is used for storing the results of template expressions in an
// array, prop value for Components, or VRoot and VList props.
readonly p : P ;
}
type VArray = VAny [ ] ;
type VAny =
| null // empty slot
| undefined // empty slot
| false // empty slot
| string // text
| number // text
| VRoot // VNode<RootDescriptor, RootProps>
| VTemplate // VNode<TemplateDescriptor, P>
| VComponent // VNode<ComponentDescriptor, P>
| VContext // VNode<ContextDescriptor, ContextProps<T>>
| VList // VNode<ListDescriptor, ListProps<K>>
| VArray // VAny[]
;
// 20 bytes
// Root Props stores a location where its children should be rendered.
interface RootProps {
// Parent Element
p : Element ,
// Next Node
n : Node | null ,
}
// 20 bytes
// Context Props stores a context value and stateless child node.
interface ContextProps < T > {
// Context value
v : T ;
// Stateless child
c : VAny ;
}
// 20 bytes
interface ListProps < K > {
// Keys that uniquely identify each stateless node in a dynamic list.
k : K [ ] ,
// Stateless nodes
v : VAny [ ] ,
} Para cada nó apátrida VAny existe um SNode com estado de Estado que possui uma interface:
// 32 bytes
interface SNode1 < V extends VAny , S1 > {
// Stateless node associated with the current state.
v : V ;
// Bitflags
f : Flags ; // SMI value - Small Integer
// Children nodes.
c : SNode | ( SNode | null ) [ ] | null ;
// Parent node.
p : SNode | null ,
// State Slot #1.
s1 : S1 ;
}
// 36 bytes
interface SNode2 < V = VAny , S1 = any , S2 = any > extends SNode1 < V , S1 > {
// State slot #2.
s2 : S2 ;
}
// Stateful Nodes are using two different shapes. Call-sites that accessing its
// flags to determine node type will be in a polymorphic state. In this case it
// is perfectly fine to use polymorphic call-sites to reduce memory usage.
type SNode < V = VAny > = SNode1 < V > | SNode2 < V > ;
// Additional state size of the root nodes depends on the implementation of
// root nodes. Default root implementation doesn't use any additional state and
// stores `null` value in the additional state slot.
type SRoot < S > = SNode1 < VRoot , S > ;
// Text nodes are storing a reference to a Text DOM node.
type SText = SNode1 < string | number , Text > ;
// Template nodes are storing a reference to a root DOM node, DOM nodes with
// dynamic properties and DOM nodes that will be used as a reference for
// `parent.insertBefore(node, nextNode)` operations. Slots for DOM nodes with
// dynamic properties that also used as a reference for insertBefore operation
// will share the same slots, there won't be any duplicated references.
type STemplate = SNode1 < VTemplate , Node [ ] > ;
// Dynamic lists doesn't have any additional state.
type SList = SNode1 < VList , null > ;
// Components are using State Nodes with 2 state slots.
type SComponent = SNode2 <
VComponent ,
// Render function.
//
// Stateless components will share the same function.
// Stateful components will create closures and its memory usage will depend
// on the size of the closure context.
null | ( ( props : any ) => VAny ) ,
// Unmount hooks.
//
// Usually components don't have any unmount hooks, or they have just one
// unmount hook.
//
// When there is one hook, it will be stored without any additional arrays.
// If we add one more hook, array will be preallocated with exactly two
// slots `[firstHook, newHook]`. And when it grows even more, javascript
// engine will preallocate internal storage using a growth factor[1][2].
//
// 1. https://en.wikipedia.org/wiki/Dynamic_array#Growth_factor
// 2. https://github.com/v8/v8/blob/1e6775a539a3b88b25cc0ffdb52529c68aad2be8/src/objects/js-objects.h#L584-L590
null | ( ( ) => void ) | ( ( ) => void ) [ ]
> ;
// Contexts doesn't have any additional state.
type SContext = SNode1 < null , null > ;Essas estruturas de dados foram cuidadosamente projetadas para ter uma pequena sobrecarga de memória e evitar muitos sites de chamada polimórficos/megórficos que acessam essas estruturas de dados.
Para entender por que os sites de chamada monomórficos são importantes para o desempenho, é recomendável ler um ótimo artigo sobre este tópico: "O que há com o monomorfismo?".
Os modelos são pré -compilados em uma parte estática que é armazenada em um objeto TemplateDescriptor e em uma variedade de expressões dinâmicas.
const Example = ( attr , child ) => html `div :attr= ${ attr } span ${ child } ` ;É compilado em:
// _T() creates TemplateDescriptor
const _tpl_1 = _T (
// _h() creates a template factory that uses Node.cloneNode(true) to
// instantiate static template structure.
_h ( "<div><span></span></div>" ) ,
// SMI (Small Integer) value that packs several values:
// struct Data {
// stateSize:6; // The number of state slots
// childrenSize:6; // The number of children slots
// svg:1; // Template with SVG elements
// }
// stateSize and childrenSize are used for preallocating arrays with
// exact number to avoid dynamic growth and reduce memory consumption.
1026 ,
// propOpCodes is an array of SMI values that stores opCodes for updating
// element properties.
[ 2 ] ,
// childOpCodes is an array of SMI values that stores opCodes for updating
// children nodes.
[ 7 , 4 ] ,
// stateOpCodes is an array of SMI values that stores opCodes for traversing
// DOM nodes and saving references to DOM nodes into internal state when
// template is instantiated.
[ 4 ] ,
// An array of string values that stores attribute name, event names, etc.
[ "attr" ] ,
) ;
// _t() creates stateless tree node VTemplate with shared TemplateDescriptor
// and an array of dynamic expressions.
const Example = ( attr , child ) => _t ( _tpl_1 , [ attr , child ] ) ; // Descriptor with TemplateData and template factory function.
type TemplateDescriptor = VDescriptor < TemplateData , ( ) => Element > ;
interface TemplateData {
// stateSize / childrenSize / svg flag
f : number ,
// Prop OpCodes
p : PropOpCode [ ] ,
// Child OpCodes
c : ChildOpCode [ ] ,
// State OpCodes
s : StateOpCode [ ] ,
// Strings
d : string [ ] ,
}
// Stateless tree node VTemplate.
type VTemplate < P = any > = VNode < TemplateDescriptor , P > ;O Compilador de Modelos não apenas elimina a etapa de compilação durante o tempo de execução, ele também leva os atributos estáticos e os ouvintes de eventos, desduplicam os códigos, strings e funções de fábrica de modelos. Por exemplo
import { className } from "styles.css" ;
const a = ( id ) => html `
< div class = ${ className } id = ${ id } > </ div >
` ;
const b = ( id ) => html `
< div class = ${ className } id = ${ id } > </ div >
` ;Gerará dois modelos diferentes com estruturas de dados compartilhadas:
import { className } from "styles.css" ;
import { _h , _T , _t } from "ivi" ;
const EMPTY_ARRAY = [ ] ;
const __IVI_STRINGS__ = [ "id" ] ;
const ELEMENT_FACTORY_1 = _h ( '<div class="' + className + '"></div>' ) ;
const SHARED_OP_CODES_1 = [ /*..*/ ] ;
const _tpl_a = _T (
/* factory */ ELEMENT_FACTORY_1 ,
/* flags */ 0 ,
/* propOpCodes */ SHARED_OP_CODES_1 ,
/* childOpCodes */ EMPTY_ARRAY ,
/* stateOpCodes */ EMPTY_ARRAY ,
/* shared strings */ __IVI_STRINGS__ ,
) ;
const _tpl_b = _T (
/* factory */ ELEMENT_FACTORY_1 ,
/* flags */ 0 ,
/* propOpCodes */ SHARED_OP_CODES_1 ,
/* childOpCodes */ EMPTY_ARRAY ,
/* stateOpCodes */ EMPTY_ARRAY ,
/* shared strings */ __IVI_STRINGS__ ,
) ;
const a = ( id ) => _t ( _tpl_a , [ id ] ) ;
const b = ( id ) => _t ( _tpl_b , [ id ] ) ;Muitas vezes, os códigos de OPCs usados para diferentes fins (adereços, crianças, estado) terão valores semelhantes; portanto, quando os códigos de operação são deduplicados, eles são tratados como matrizes simples com números inteiros que podem ser usados para propósitos diferentes.
Os strrings compartilhados (teclas de atributo, nomes de eventos, etc.) são desduplicados em uma matriz ( __IVI_STRINGS__ ) compartilhada entre todos os modelos.
O IVI é projetado como uma solução incorporável, para que possa ser integrada às estruturas ou componentes da Web existentes. A função básica do nó raiz instanciada com a função createRoot() está usando a fila Microtosk para agendar atualizações. Os nós raiz com o algoritmo de agendamento personalizado podem ser criados definindo novas fábricas raiz com a função defineRoot() .
function defineRoot ( onInvalidate : ( root : Root < undefined > ) => void )
: ( parentElement : Element , nextNode : Node | null ) => Root < undefined > ;
function defineRoot < S > ( onInvalidate : ( root : Root < S > ) => void )
: ( parentElement : Element , nextNode : Node | null , state : S ) => Root < S > ;Como exemplo, para remover qualquer lote e atualizar imediatamente a subárvore root quando estiver invalidado, podemos definir o seguinte nó raiz:
import { defineRoot } from "ivi" ;
const createSyncRoot = defineRoot ( ( root ) => {
// Immediately triggers dirty checking.
dirtyCheck ( root ) ;
} ) ; requestAnimationFrame() para agendar atualizações da interface do usuário O algoritmo de agendamento com lotes rAF possui algumas pistolas em potencial com condições de corrida.
function formStateReducer ( state , action ) {
switch ( action . type ) {
case "update" :
return {
value : action . value ,
valid : / ^[a-z]+$ / . test ( action . value ) ,
} ;
}
return state ;
}
const Form = component ( ( c ) => {
const [ state , dispatch ] = useReducer ( c ,
{ value : "" , valid : false } ,
formStateReducer ,
) ;
const onInput = ( ev ) => {
dispatch ( { type : "update" , value : ev . target . value } ) ;
} ;
return ( ) => html `
< form >
< input
@input = ${ onInput }
*value = ${ state ( ) . value }
/>
< input
type =" submit "
value =" Submit "
.disabled = ${ ! state ( ) . valid }
/ >
</ form >
` ;
} ) ;
update (
createRoot ( document . getElementById ( "app" ) ! ) ,
Form ( ) ,
) ; Neste exemplo, se o usuário digitar muito rápido e pressionar um botão [enter] , é possível obter uma ordem de execução como esta:
0 em <input> .onChange() é acionado, state.valid muda para um estado false .[enter] .<input type="submit" .disabled={false} />rAF é acionado, o botão Enviar entra no estado desativado. A maneira mais simples de evitar problemas como esse é usar microtasks para lotes. Mas se você realmente deseja adicionar agendamento rAF , é possível resolver questões como essa, introduzindo alguns primitivos de sincronização:
import { uiReady } from "my-custom-scheduler" ;
const onSubmit = async ( ev ) => {
await uiReady ( ) ;
submit ( ) ;
} ; O tempo de execução do IVI não depende de nenhuma biblioteca externa.
IVI Dev Tools tem um conjunto mínimo de dependências:
@rollup/pluginutils é usado em um plug-in Rollup @ivi/rollup-plugin para filtrar os módulos. Mit