IVI es una biblioteca de interfaz de interfaz de intermedia declarable integrable liviana.
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 ( ) ,
) ;El tamaño del ejemplo precompilado anterior es de solo 2.7kb (minificado+brotli). Incluye tiempo de ejecución completo para la representación de la interfaz de usuario declarativa. Las plantillas precompiladas están optimizadas para el tamaño del código y el rendimiento de arranque en frío.
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)Las plantillas IVI funcionarán sin precompilación, pero se recomienda altamente utilizar la precompilación para mejorar el rendimiento y reducir el tamaño del código.
El paquete "@ivi/vite-plugin" proporciona un complemento VITE.
// vite.config.mjs
import { defineConfig } from "vite" ;
import { ivi } from "@ivi/vite-plugin" ;
export default defineConfig ( {
plugins : [ ivi ( ) ] ,
} ) ; El paquete "@ivi/rollup-plugin" proporciona un complemento de rollo.
// rollup.config.mjs
import { ivi } from "@ivi/rollup-plugin" ;
export default {
input : "src/main.js" ,
output : {
file : "bundle.js" ,
} ,
plugins : [ ivi ( ) ]
} ; El lenguaje de plantilla IVI tiene una sintaxis similar a HTML con sintaxis adicional para propiedades DOM, eventos y eliminación de espacios en blanco.
html crea una plantilla con nodos htmlelement.svg crea una plantilla con nodos SVGelement. import { html } from "ivi" ;
const Example = component ( ( c ) => {
// ...
return ( ) => html `
< div class =" app " >
< div > ${ count ( ) } </ div >
< button @click = ${ inc } > Increment </ button >
</ div >
` ;
} ) ;Las plantillas pueden tener múltiples nodos raíz.
html `
< div > </ div >
${ expr }
text
< div > </ div >
` Los elementos sin hijos se pueden cerrar con sintaxis 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 evita eliminar todos los espacios en blanco alrededor de las nuevas líneas: < div >
< b > 1 </ b >
v item left
< div > < div > < b > 1 </ b > item left </ div >En plantillas IVI, puede incluir contenido dinámico llamado expresiones. Una expresión es solo una pieza de código JavaScript que se evalúa cuando se representa la plantilla. Cualquier valor que produce una expresión en ese momento se incluirá en la plantilla renderizada final.
html `
< div attr = ${ attributeValueExpr } >
${ childExpr }
</ div > ` ;El lenguaje de plantilla IVI admite una sintaxis adicional para trabajar con propiedades DOM, eventos, etc.
<div name="value" /> - atributo estático.<div name /> - Atributo estático.<div name=${expr} /> - atributo dinámico element.setAttribute(name, expr) .<div .name=${expr} /> - element[name] = expr .<div *name=${expr} /> - element[name] = expr , difiere contra un valor DOM.<div ~name="value" /> - estilo estático <div style="name:value;"> .<div ~name=${expr} /> - Dynamic Style element.style.setProperty(name, expr) .<div @name=${expr} /> - Event element.addEventListener(name, expr) .<div ${directive} /> - directive(element) .<div .textContent=${expr} /> - Contenido de texto. <div name="value" /> - atributo estático con un valor <div name="value"> .<div name /> - Atributo estático sin un valor <div name> .<div name=${expr} /> - atributo dinámico element.setAttribute(name, expr) . Los atributos DOM se asignan con Element.setAttribute(..) .
Cuando el atributo dinámico tiene un valor undefined , null o false , se eliminará del elemento DOM con el método Element.removeAttribute(..) .
<div .name=${expr} /> - element[name] = expr .<div *name=${expr} /> - element[name] = expr , difiere contra un valor DOM. Las propiedades se asignan con un operador de asignación Element.name = value .
La diferencia con un valor DOM es útil en los casos de uso cuando usamos valores <input> para evitar activar eventos input innecesarios.
<div ~name="value" /> - Static Style <div style="value"> .<div ~name=${expr} /> - Dynamic Style element.style.setProperty(name, expr) . Los estilos estáticos se fusionan automáticamente con el atributo :style="value" .
Los estilos dinámicos se asignan con un método CSSStyleDeclaration.setProperty(..) .
Cuando el estilo tiene un valor undefined , null o false , se eliminará con el método CSSStyleDeclaration.removeProperty(..) .
<div @name=${expr} /> - Event element.addEventListener(name, expr) . Los eventos se asignan con un método EventTarget.addEventListener(..) .
Cuando el evento tiene un valor undefined , null o false , se eliminará con el método EventTarget.removeEventListener(..) .
<div .textContent=${expr} /> - Text Content element.textContent = expr . La propiedad de contenido de texto se puede utilizar como una optimización que reduce ligeramente el consumo de memoria para elementos con un niño de texto. Creará un nodo de texto con una propiedad Node.textContent y no tendrá nodos con estado asociados con un nodo de texto.
El valor de contenido de texto debe tener un undefined , null , false , string o un tipo number .
<div ${directive} /> - directive(element) .La directiva es una función que se invoca cada vez que la plantilla de tiempo se actualiza y recibe un elemento DOM asociado con una directiva:
type ElementDirective = < E extends Element > (
element : E ,
) => void ;La función de la directiva se invoca solo cuando la plantilla se crea con una función diferente, por lo que si vamos a reutilizar la misma función, se puede usar como una devolución de llamada creada por el elemento DOM:
const Example = component ( ( c ) => {
const onCreated = ( innerElement ) => {
// ..
} ;
return ( ) => html `
< div >
< div class =" Inner " ${ onCreated } />
</ div >
` ;
} ) ;Las directivas se pueden usar no solo como una simple devoluciones de llamada creadas por DOM, sino también como directivas con estado. P.ej
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 ) } />
` ;
} ) ;Puede usar expresiones regulares de JavaScript en sus plantillas, lo que significa que puede usar cualquier construcción de flujo de control de JavaScript, como operadores condicionales, llamadas de funciones y las declaraciones de si o cambiar para generar contenido dinámico en función de las condiciones de tiempo de ejecución.
Esto significa que puede crear plantillas con una lógica compleja que condicionalmente representa contenido diferente en función de lo que está sucediendo en su aplicación. Puede anidar expresiones de plantillas dentro de la otra para construir plantillas más complejas, y puede almacenar los resultados de las plantillas en variables para usarlas más adelante en su código.
const Example = component ( ( c ) => {
// ...
return ( show ) => html `
< div >
${ show && html ` < span > Show </ span > ` }
</ div >
` ;
} ) ;Si se usa una expresión en la posición infantil de un elemento HTML y devuelve una matriz, IVI representará todos los elementos en esa matriz como nodos separados.
const Example = ( ) => html `
< div >
${ [
"Text Node 1" ,
"Text Node 2" ,
] }
</ div >
` ;IVI permite que los componentes devuelvan matrices de elementos como nodos raíz. Esto significa que un componente puede devolver múltiples elementos de nivel superior en lugar de solo uno.
Por ejemplo, un componente podría devolver una matriz de elementos <li> que forman una lista. Cuando se representa este componente, IVI tratará la matriz de elementos <li> como un conjunto de elementos de nivel superior, al igual que lo haría con un elemento de raíz único.
Esta característica proporciona más flexibilidad al construir componentes de interfaz de usuario complejos, ya que le permite crear componentes que generen un número dinámico de elementos de nivel superior dependiendo de su entrada.
const Example = component ( ( c ) => {
return ( entries ) => entries . map ( ( e ) => html `
< li > ${ e } </ li >
` ) ;
) ;
// Example([1, 2, 3])Cuando se actualizan las matrices, su posición en la matriz se asigna los nodos de árbol sin estado en sus nodos con estado.
Cuando la matriz contiene una expresión condicional que devuelve un valor de "agujero" ( null , undefined o false ), el agujero ocupará una ranura en un árbol con estado, de modo que todos los nodos se asignen correctamente a sus nodos con estado.
[
conditional ? "text" : null ,
StatefulComponent ( ) ,
] En el ejemplo anterior, cuando la expresión conditional va de un texto a un "agujero" y viceversa, StatefulComponent preservará su estado interno.
Cuando la matriz crece o se encoge en tamaño, se crearán o eliminarán nodos con estado al final de una matriz.
En IVI, puede representar listas de elementos utilizando la función List() que boje a través de una matriz de datos y devuelve una lista de elementos. Sin embargo, cuando se actualiza la lista, es importante mapear correctamente los elementos en sus opiniones con estado. Esto significa que si un elemento se representa como un componente que tiene un estado interno que podría cambiar como resultado de acciones del usuario o eventos externos, debe asignarse a la misma instancia de componente.
Para representar listas dinámicas, IVI proporciona la función 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 ;Crea una lista dinámica con una variedad de claves que identifican de manera única cada elemento en la lista. Cuando se actualiza la lista, IVI usa claves para asignar elementos en sus nodos con estado.
Es importante tener en cuenta que al representar una lista dinámica, siempre debe usar un identificador único como clave. Esto ayuda a IVI a identificar cada elemento en una lista y evitar los errores de representación. Si usa un índice o un valor aleatorio como clave, es posible que IVI no pueda identificar elementos correctos en una lista, lo que puede causar errores.
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 >
` ; IVI está utilizando un algoritmo óptimo para listas dinámicas que utiliza el número mínimo de operaciones de Node.insertBefore() para reorganizar los nodos DOM.
Reducir las operaciones Node.insertBefore() es importante no solo porque invalida el estado DOM interno, sino también porque cada vez que se mueve uno de los nodos DOM conectados al documento, puede producir una notificación de servidor mutationObs. Y muchas extensiones populares están utilizando observadores de mutaciones para observar un subárbol de documento completo, por lo que cada operación insertBefore puede volverse bastante costoso cuando se usa fuera de las cajas de arena de la evaluación comparativa.
Los componentes pueden ser con estado o apátridos. Los componentes con estado se utilizan cuando necesita administrar el estado que cambia con el tiempo, como la entrada del usuario, las solicitudes de red o las animaciones.
Los componentes con estado se declaran con la función component() . Crea una función de fábrica que produce nodos 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" } ) ,
) ;Los componentes con estado están utilizando los cierres de JavaScript para almacenar el 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 >
` ;
} ) ; Cuando el estado interno está mutado, no activa las actualizaciones de los componentes automáticamente y debe invalidar manualmente con la función invalidate() .
Hay API de alto nivel como useState() o useReducer() que usan la función invalidate() de bajo nivel detrás de escena para invalidar automáticamente los componentes cuando el estado interno está 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 > `
) ;
} ) ;Los componentes sin estado en IVI son solo funciones básicas de JavaScript. Son más rápidos y livianos que los componentes con estado, lo que los convierte en una buena opción para componentes simples y reutilizables que no tienen ningún 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 ;Un nodo raíz es el nodo más alto en un árbol con estado, desde el cual se renderizan todos los demás nodos. Representa un punto de entrada para el algoritmo de representación IVI y almacena una posición en el árbol DOM.
createRoot() createRoot crea un nodo raíz que usa la cola de microtask para programar actualizaciones.
function createRoot (
parentElement : Element ,
nextNode : Node | null = null ,
) : Root ;parentElement - Elemento DOM de padres.nextNode - Next Node DOM. dirtyCheck() dirtyCheck realiza el algoritmo de verificación Dirty en un subárbol raíz y actualiza todos los componentes sucios.
function dirtyCheck (
root : Root ,
forceUpdate : boolean = false ,
) : void ;root - nodo raíz.forceUpdate : obliga a todos los componentes a actualizar, incluso cuando están utilizando sugerencias de optimización para reducir las actualizaciones. update() update actualizaciones un subárbol raíz con una nueva representación.
function update (
root : Root ,
v : VAny ,
forceUpdate : boolean = false ,
) : void ;root - nodo raíz.v - nueva representación.forceUpdate : obliga a todos los componentes a actualizar, incluso cuando están utilizando sugerencias de optimización para reducir las actualizaciones. unmount() unmount desmonta un subárbol raíz del DOM y desencadena ganchos desmontados en los componentes.
function unmount (
root : Root ,
detach : boolean ,
) : void ;root - nodo raíz.detach : separe los nodos DOM más altos del subárbol DOM. defineRoot() defineRoot crea una fábrica de nodo raíz que utiliza un gancho personalizado OnRootInvalidated .
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 : gancho OnRootInvalidated que recibe un nodo raíz y un estado personalizado asociado con ese nodo raíz.component() component crea una fábrica que produce nodos 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 : función que produce funciones de renderizado de componentes con estado.areEqual - FUNCIÓN OPCIONAL QUE verifica las propiedades de entrada para los cambios y se usa como una pista de optimización para reducir las actualizaciones innecesarias cuando las propiedades no cambiaron. Cuando Root Sustree se actualiza con la opción forceUpdate , se ignora areEqual sugerencia y todos los componentes se actualizan.
getProps() getProps obtiene accesorios de componentes actuales de la instancia de componente.
function getProps = < P > ( component : Component < P > ) : P ;component - Instancia de componente. invalidate() invalidate el componente invalidados y programar una actualización.
function invalidate ( component : Component ) : void ;component - Instancia de componente. useUnmount()Agrega un gancho desmontado.
function useUnmount (
component : Component ,
hook : ( ) => void ,
) : void ;component - Instancia de componente.hook - Angro de hábito.useMemo() useMemo crea una función memoizada.
function useMemo < T , U > (
areEqual : ( prev : T , next : T ) => boolean ,
fn : ( props : T ) => U ,
) : ( props : T ) => U ;areEqual - Comprueba las propiedades de entrada para obtener cambios para evitar las recomputaciones.fn - función para memoizar. useState() useState crea un estado de componente reactivo.
function useState < S > (
component : Component ,
state : S ,
) : [
get : ( ) => S ,
set : ( s : S ) => void ,
] ;component - Instancia de componente.state - Estado inicial.Devuelve las funciones estatales de Getter y State Setter.
useReducer() useReducer crea un reductor de estado de componente reactivo.
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 - Instancia de componente.state - Estado inicial.reducer - Función de reductor de estado.Devuelve las funciones de State Getter y del despachador de acción.
Los efectos secundarios le permiten especificar cómo sus componentes deben comportarse con sistemas externos como llamadas de API imperativas, manipulaciones de temporizador o interacciones DOM directas.
Puede pensar en ello como una combinación de ganchos mount , update y lifeciclos unmount .
useEffect() useEffect crea un efecto secundario que se ejecuta inmediatamente después de que el nodo raíz finalice una actualización.
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 - Instancia de componente.effect - Efecto Gancho.areEqual : la función opcional que verifica las propiedades de entrada para los cambios y se usa para controlar cuándo se debe actualizar un efecto.Devuelve una función de efecto secundario que debe invocarse en una función de renderizado.
useLayoutEffect() useLayoutEffect crea un efecto secundario que se ejecuta antes del marco de animación.
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 - Instancia de componente.effect - Efecto Gancho.areEqual : la función opcional que verifica las propiedades de entrada para los cambios y se usa para controlar cuándo se debe actualizar un efecto.Devuelve una función de efecto secundario que debe invocarse en una función de renderizado.
useIdleEffect() useIdleEffect crea un efecto secundario que se ejecuta cuando el navegador está inactivo.
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 - Instancia de componente.effect - Efecto Gancho.areEqual : la función opcional que verifica las propiedades de entrada para los cambios y se usa para controlar cuándo se debe actualizar un efecto.Devuelve una función de efecto secundario que debe invocarse en una función de renderizado.
List() List crea listas dinámicas.
function List < E , K > (
entries : E [ ] ,
getKey : ( entry : E , index : number ) => K ,
render : ( entry : E ) => VAny ,
) : VList ;entries - Datos de entrada.getKey : función que debería devolver una clave única para cada entrada de datos.render - función que representa una entrada.context() context crea funciones de getter y proveedor de contexto de contexto.
function context = < T > ( ) : [
get : ( component : Component ) => T | undefined ,
provider : ( value : T , children : VAny ) => VContext < T > ,
] Devuelve una función get que encuentra el valor de contexto más cercano y una función provider que crea nodos 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 es una escotilla de escape que permite extender el algoritmo de representación IVI.
type ElementDirective = < E extends Element > (
element : E ,
) => void ;eventDispatcher() eventDispatcher crea un despachador de eventos que encuentra el nodo DOM Child más cercano y emite un CustomEvent con el 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 : opciones de evento que se utilizarán cuando se envíe el evento.El despachador de eventos invoca a los controladores de eventos sincrónicamente. Todos los manejadores de eventos se invocan antes de que regrese el despachador de eventos.
findDOMNode() findDOMNode encuentra el niño del nodo DOM más cercano que pertenece a un subárbol de nodo con estado.
function findDOMNode < T extends Node | Text > (
node : SNode | null ,
) : T | null ;node - Nodo con estado. containsDOMElement() containsDOMElement verificaciones de control si un nodo con estado contiene un elemento DOM en su subárbol.
function containsDOMElement (
node : SNode ,
element : Element ,
) : boolean ;node - Nodo con estado.element - Elemento DOM. hasDOMElement() hasDOMElement verifica si un nodo con estado tiene un elemento DOM como su hijo.
function hasDOMElement (
node : SNode ,
child : Element ,
) : boolean ;node - Nodo con estado.child - Elemento DOM.preventUpdates() preventUpdates es una función NOOP que siempre devuelve el valor true .
function preventUpdates < T > ( a : T , b : T ) : true ; strictEq() strictEq verifica valores de igualdad con estricto operador de igualdad === .
function strictEq < T > ( a : T , b : T ) : boolean ; shallowEq() shallowEq verifica los objetos con algoritmo de igualdad superficial y utiliza un operador de igualdad estricto para verificar los valores individuales para la igualdad.
function shallowEq < T extends object > ( a : T , b : T ) : boolean ; shallowEqArray() shallowEqArray verifica las matrices con algoritmo de igualdad superficial y utiliza un operador de igualdad estricto para verificar los valores individuales de igualdad.
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 ( ) ,
) ; El algoritmo de invalidación de componentes se implementa marcando el componente como sucio y marcando todos sus nodos parentales con una bandera que tienen un subárbol sucio. Cuando el algoritmo de marcado llega a un nodo raíz, invoca un gancho OnRootInvalidated() que puede usarse para implementar un programador personalizado.
Dirty .DirtySubtree .DirtySubtree , gancho OnRootInvalidated() invocado.Dirty , padres ya marcados con bandera DirtySubtree .Cuando Scheduler decide actualizar un nodo raíz con un subárbol sucio, comienza un algoritmo de verificación sucio. Este algoritmo va de arriba hacia abajo en un orden de derecha a izquierda, visitando todos los nodos con una bandera de subárbol sucia hasta que alcanza un componente sucio y lo actualiza.
DirtySubtree , comienza a verificar a sus hijos.Dirty , desencadena una actualización.Dirty , desencadena una actualización. Una de las razones por las cuales la biblioteca principal es tan pequeña es porque el algoritmo de actualización se implementa en el orden RTL. Algoritmo que realiza actualizaciones en el orden RTL simplifica muchos problemas complejos con las actualizaciones de DOM. El problema principal con las actualizaciones de DOM es que cuando comenzamos a actualizar una estructura de árbol DOM, necesitamos tener una referencia a un padre y un próximo nodo DOM, para que podamos usar parent.insertBefore(newNode, nextNode) . En la mayoría de los casos, es fácil recuperar un próximo nodo DOM, pero hay casos de borde como cuando tenemos dos expresiones condicionales adyacentes y uno de sus estados es que elimina completamente un nodo DOM del árbol, o dos componentes adyacentes con condicionales en sus raíces, etc.
La mayoría de las bibliotecas están tratando con estos casos de borde al introducir nodos DOM marcadores (comentarios o un nodo de texto vacío). Por ejemplo, para implementar expresiones condicionales, podemos agregar un nodo de texto vacío cuando el condicional no rinde ningún nodo DOM y cuando el condicional entra en un estado cuando necesita agregar un nodo DOM, usará un nodo marcador como referencia de nodo DOM siguiente. El algoritmo de actualización RTL en IVI no usa ningún nodo marcador.
El algoritmo RTL que se usa en IVI también hace que sea mucho más fácil implementar desplazamientos de nodos sin introducir ninguna ruta de código adicional, fragmentos y casi todo lo que implica actualizar una estructura DOM.
Cada sitio de llamadas que crea una plantilla tiene una identidad única, por lo que incluso las plantillas idénticas creadas a partir de diferentes sitios de llamadas no podrán diferir entre sí.
function TemplateUniqueIdentity ( condition , text ) {
return ( condition )
? html `div ${ text } `
: html `div ${ text } ` ;
} En el ejemplo anterior, cuando se cambia condition , en lugar de actualizar el nodo de texto, el algoritmo de actualización reemplazará todo el elemento DIV con uno nuevo.
Hay algunos casos de uso que requieren muchas lecturas frecuentes de una variable reactiva. Y cada vez que esta variable cambia, afecta a muchos nodos de la interfaz de usuario, como cambiar entre temas de luz/oscuridad.
En lugar de crear muchas suscripciones a estas variables, se recomienda utilizar valores simples de JavaScript y un subtree de interfaz de usuario entero de nuevo con dirtyCheck(root, true) cuando se cambian estos valores.
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 ( ) ) ; La clonación de la plantilla es una optimización que se utiliza para clonar plantillas HTML con un método Node.cloneNode() .
Por defecto, la clonación de plantillas está habilitada para todas las plantillas. Pero a veces sería un desperdicio crear una plantilla para clonarse e instanciarse cuando esta plantilla se representa solo una vez.
Para deshabilitar la clonación, la plantilla debe tener un comentario líder /* preventClone */ . P.ej
const Example = ( ) => /* preventClone */ html `
< div class =" Title " > ${ text } </ div >
` ; Se crearán plantillas con un solo elemento que no tiene ninguna propiedad estática con document.createElement() .
html ` < div attr = ${ 0 } > ${ 1 } </ div > ` ;Por defecto, los controladores de eventos (expresiones de la función de flecha) se elevan automáticamente al alcance más externo.
const Example = component ( ( c ) => {
const [ count , setCount ] = useState ( c , 0 ) ;
return ( ) => html `
< div @click = ${ ( ) => { setCount ( count ( ) + 1 ) ; } } > ${ count ( ) } </ div >
` ;
} ) ;Después de la elevación del controlador de eventos, se transformará en:
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 deshabilitar los manipuladores de eventos, la plantilla debe tener un comentario líder /* preventHoist */ . P.ej
const Example = component ( ( c ) => {
const [ count , setCount ] = useState ( c , 0 ) ;
return ( ) => /* preventHoist */ html `
< div @click = ${ ( ) => { setCount ( count ( ) + 1 ) ; } } > ${ count ( ) } </ div >
` ;
} ) ; Se pueden declarar múltiples anotaciones separándolas con | Operador, por ejemplo /* preventClone | preventHoist */
Para obtener una estimación aproximada del uso de la memoria, es importante comprender las estructuras de datos internos.
En la descripción a continuación, vamos a calcular el uso de la memoria en un motores basados en el cromo con compresión del puntero en V8.
UI Tree se implementa con un árbol con estado SNode y un VAny inmutable sin estatuto.
El árbol sin estado tiene una estructura de datos simple:
// 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 nodo apátrido VAny hay un SNode con estado que tiene una interfaz:
// 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 > ;Estas estructuras de datos fueron cuidadosamente diseñadas para tener una pequeña sobrecarga de memoria y evitar muchos sitios de llamadas polimórficas/megamórficas que acceden a estas estructuras de datos.
Para comprender por qué los sitios de llamadas monomórficas son importantes para el rendimiento, se recomienda leer un gran artículo sobre este tema: "¿Qué pasa con el monomorfismo?".
Las plantillas se precompilan en una parte estática que se almacena en un objeto TemplateDescriptor y una matriz de expresiones dinámicas.
const Example = ( attr , child ) => html `div :attr= ${ attr } span ${ child } ` ;Se compila en:
// _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 > ;El compilador de plantillas no solo elimina el paso de compilación durante el tiempo de ejecución, sino que también eleva los atributos estáticos y los oyentes de eventos, dedica códigos de operación, cadenas y funciones de fábrica de plantillas. P.ej
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 >
` ;Generará dos plantillas diferentes con estructuras de datos compartidas:
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 ] ) ;Muy a menudo, los códigos de operación que se usan para diferentes propósitos (accesorios, hijos, estado) tendrán valores similares, por lo que cuando los códigos de operación se dedican, se tratan como matrices simples con enteros que pueden usarse para diferentes propósitos.
Las strrings compartidas (claves de atributo, nombres de eventos, etc.) se dedican en una matriz ( __IVI_STRINGS__ ) que se comparte entre todas las plantillas.
IVI está diseñado como una solución integrable, para que pueda integrarse en marcos o componentes web existentes. El nodo raíz básico instanciado con la función createRoot() es usar MicroTask Queue para programar actualizaciones. Los nodos raíz con algoritmo de programación personalizado se pueden crear definiendo nuevas fábricas raíz con la función 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 ejemplo, para eliminar cualquier grupo de lotes e inmediatamente actualizar el subárbol raíz cuando se invalida, podemos definir el siguiente nodo raíz:
import { defineRoot } from "ivi" ;
const createSyncRoot = defineRoot ( ( root ) => {
// Immediately triggers dirty checking.
dirtyCheck ( root ) ;
} ) ; requestAnimationFrame() para programar actualizaciones de la interfaz de usuario El algoritmo de programación con lotes rAF tiene algunas pistas de reposo potenciales con condiciones de carrera.
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 ( ) ,
) ; En este ejemplo, si los tipos de usuario realmente rápido y presionan un botón [enter] , es posible obtener un orden de ejecución como este:
0 en <input> .onChange() se activa, state.valid cambia a un estado false .[enter] .<input type="submit" .disabled={false} />rAF se activa, el botón Enviar entra en el estado de discapacitados. La forma más sencilla de evitar problemas como este es usar microtasks para lotes. Pero si realmente desea agregar la programación rAF , es posible resolver problemas como este introduciendo algunas primitivas de sincronización:
import { uiReady } from "my-custom-scheduler" ;
const onSubmit = async ( ev ) => {
await uiReady ( ) ;
submit ( ) ;
} ; El tiempo de ejecución de IVI no depende de ninguna biblioteca externa.
IVI Dev Tools tiene un conjunto mínimo de dependencias:
@rollup/pluginutils se usa en un complemento de rollo @ivi/rollup-plugin para filtrar módulos. MIT