IVI est une bibliothèque interne déclarative légère légère.
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 ( ) ,
) ;La taille de l'exemple précompilé ci-dessus est seulement de 2,7 Ko (minifié + brotli). Il comprend l'intégralité de l'exécution pour le rendu de l'interface utilisateur déclaratif. Les modèles précompilés sont optimisés pour la taille du code et les performances de démarrage à froid.
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)Les modèles IVI fonctionneront sans aucune précompilation, mais il est fortement recommandé d'utiliser la précompilation pour améliorer les performances et réduire la taille du code.
Le package "@ivi/vite-plugin" fournit un plugin VITE.
// vite.config.mjs
import { defineConfig } from "vite" ;
import { ivi } from "@ivi/vite-plugin" ;
export default defineConfig ( {
plugins : [ ivi ( ) ] ,
} ) ; Le package "@ivi/rollup-plugin" fournit un plugin Rollup.
// rollup.config.mjs
import { ivi } from "@ivi/rollup-plugin" ;
export default {
input : "src/main.js" ,
output : {
file : "bundle.js" ,
} ,
plugins : [ ivi ( ) ]
} ; Le langage du modèle IVI a une syntaxe de type HTML avec une syntaxe supplémentaire pour les propriétés DOM, les événements et l'élimination des espaces blancs.
html crée un modèle avec des nœuds htmlelement.svg crée un modèle avec des nœuds SVGelement. import { html } from "ivi" ;
const Example = component ( ( c ) => {
// ...
return ( ) => html `
< div class =" app " >
< div > ${ count ( ) } </ div >
< button @click = ${ inc } > Increment </ button >
</ div >
` ;
} ) ;Les modèles peuvent avoir plusieurs nœuds racinaires.
html `
< div > </ div >
${ expr }
text
< div > </ div >
` Les éléments sans enfant peuvent être fermés avec une syntaxe 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 empêche de supprimer tous les espaces blancs autour de Newlines: < div >
< b > 1 </ b >
v item left
< div > < div > < b > 1 </ b > item left </ div >Dans les modèles IVI, vous pouvez inclure un contenu dynamique appelé expressions. Une expression n'est qu'un morceau de code JavaScript qui est évalué lorsque le modèle est rendu. Quelle que soit la valeur qu'une expression produite à ce moment-là sera incluse dans le modèle rendu final.
html `
< div attr = ${ attributeValueExpr } >
${ childExpr }
</ div > ` ;Le langage du modèle IVI prend en charge une syntaxe supplémentaire pour travailler avec les propriétés, événements, etc.
<div name="value" /> - attribut statique.<div name /> - attribut statique.<div name=${expr} /> - dynamic Attribut element.setAttribute(name, expr) .<div .name=${expr} /> - element[name] = expr .<div *name=${expr} /> - element[name] = expr , diffe contre une valeur DOM.<div ~name="value" /> - Style static <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} /> - Contenu texte. <div name="value" /> - attribut statique avec une valeur <div name="value"> .<div name /> - Attribut statique sans valeur <div name> .<div name=${expr} /> - dynamic Attribut element.setAttribute(name, expr) . Les attributs DOM sont attribués avec Element.setAttribute(..) .
Lorsque l'attribut dynamique a une valeur undefined , null ou false , elle sera supprimée de l'élément DOM avec Element.removeAttribute(..) .
<div .name=${expr} /> - element[name] = expr .<div *name=${expr} /> - element[name] = expr , diffe contre une valeur DOM. Les propriétés sont attribuées avec un Element.name = value .
La diffusion avec une valeur DOM est utile dans les cas d'utilisation lorsque nous utilisons des valeurs <input> pour éviter de déclencher des événements input inutiles.
<div ~name="value" /> - Style statique <div style="value"> .<div ~name=${expr} /> - dynamic Style element.style.setProperty(name, expr) . Les styles statiques sont automatiquement fusionnés avec l'attribut :style="value" .
Les styles dynamiques sont attribués avec une méthode CSSStyleDeclaration.setProperty(..) .
Lorsque le style a une valeur undefined , null ou false , elle sera supprimée avec CSSStyleDeclaration.removeProperty(..) .
<div @name=${expr} /> - event element.addEventListener(name, expr) . Les événements sont attribués avec une méthode EventTarget.addEventListener(..) .
Lorsque l'événement a une valeur undefined , null ou false , elle sera supprimée avec EventTarget.removeEventListener(..) .
<div .textContent=${expr} /> - text contenu element.textContent = expr . La propriété de contenu du texte peut être utilisée comme une optimisation qui réduit légèrement la consommation de mémoire pour les éléments avec un enfant de texte. Il créera un nœud de texte avec une propriété Node.textContent et n'aura aucun nœud avec état associé à un nœud de texte.
La valeur du contenu texte doit avoir un type undefined , null , false , string ou un type number .
<div ${directive} /> - élément Directive directive(element) .La directive est une fonction invoquée à chaque fois que le modèle est mis à jour et reçoit un élément DOM associé à une directive:
type ElementDirective = < E extends Element > (
element : E ,
) => void ;La fonction directive n'est invoquée que lorsque le modèle est créé avec une fonction différente, donc si nous voulons réutiliser la même fonction, il peut être utilisé comme rappel créé par l'élément DOM:
const Example = component ( ( c ) => {
const onCreated = ( innerElement ) => {
// ..
} ;
return ( ) => html `
< div >
< div class =" Inner " ${ onCreated } />
</ div >
` ;
} ) ;Les directives peuvent être utilisées non seulement comme des rappels créés par DOM simple, mais aussi comme des directives avec état. Par exemple
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 ) } />
` ;
} ) ;Vous pouvez utiliser des expressions JavaScript régulières dans vos modèles, ce qui signifie que vous pouvez utiliser toutes les constructions de flux de contrôle JavaScript comme les opérateurs conditionnels, les appels de fonction et les instructions IF ou Switch pour générer du contenu dynamique en fonction des conditions d'exécution.
Cela signifie que vous pouvez créer des modèles avec une logique complexe qui rend conditionnellement le contenu différent en fonction de ce qui se passe dans votre application. Vous pouvez nidifier les expressions des modèles les uns dans les autres pour construire des modèles plus complexes, et vous pouvez stocker les résultats des modèles en variables pour les utiliser plus tard dans votre code.
const Example = component ( ( c ) => {
// ...
return ( show ) => html `
< div >
${ show && html ` < span > Show </ span > ` }
</ div >
` ;
} ) ;Si une expression est utilisée dans la position enfant d'un élément HTML et qu'il renvoie un tableau, IVI rendra tous les éléments de ce tableau sous forme de nœuds séparés.
const Example = ( ) => html `
< div >
${ [
"Text Node 1" ,
"Text Node 2" ,
] }
</ div >
` ;IVI permet aux composants de retourner les tableaux d'éléments comme nœuds racinaires. Cela signifie qu'un composant peut retourner plusieurs éléments de niveau supérieur au lieu d'un seul.
Par exemple, un composant pourrait renvoyer un tableau de <li> des éléments qui composent une liste. Lorsque ce composant est rendu, IVI traitera le tableau des éléments <li> comme un ensemble d'éléments de niveau supérieur, tout comme il le ferait avec un seul élément racine.
Cette fonctionnalité offre plus de flexibilité lors de la construction de composants d'interface utilisateur complexes, car il vous permet de créer des composants qui génèrent un nombre dynamique d'éléments de niveau supérieur en fonction de leur entrée.
const Example = component ( ( c ) => {
return ( entries ) => entries . map ( ( e ) => html `
< li > ${ e } </ li >
` ) ;
) ;
// Example([1, 2, 3])Lorsque les tableaux sont mis à jour, les nœuds d'arbre sans état sont mappés sur leurs nœuds avec état par leur position dans le tableau.
Lorsque le tableau contient une expression conditionnelle qui renvoie une valeur de "trou" ( null , undefined ou false ), le trou occupera une fente dans un arbre avec état, de sorte que tous les nœuds seront correcls cartographiés sur leurs nœuds avec état.
[
conditional ? "text" : null ,
StatefulComponent ( ) ,
] Dans l'exemple ci-dessus, lorsque l'expression conditional passe d'un texte à un "trou" et vice versa, StatefulComponent préservera son état interne.
Lorsque le tableau augmente ou rétrécit la taille, les nœuds avec état seront créés ou supprimés à la fin d'un tableau.
Dans IVI, vous pouvez rendre des listes d'éléments à l'aide de la fonction List() qui parcourt un tableau de données et renvoie une liste d'éléments. Cependant, lorsque la liste est mise à jour, il est important de cartographier correctement les éléments rendus sur leurs vues avec état. Cela signifie que si un élément est rendu comme un composant qui a un état interne qui pourrait changer en raison des actions utilisateur ou des événements externes, il doit être mappé sur la même instance de composant.
Pour rendre les listes dynamiques, IVI fournit la fonction 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 ;Il crée une liste dynamique avec un tableau de clés qui identifient de manière unique chaque élément de la liste. Lorsque la liste est mise à jour, IVI utilise des clés pour cartographier les éléments sur leurs nœuds avec état.
Il est important de noter que lorsque vous rendez une liste dynamique, vous devez toujours utiliser un identifiant unique comme clé. Cela aide IVI à identifier chaque élément dans une liste et à éviter les erreurs de rendu. Si vous utilisez un index ou une valeur aléatoire comme clé, IVI peut ne pas être en mesure d'identifier les éléments corrects dans une liste, ce qui peut entraîner des erreurs.
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 utilise un algorithme optimal pour les listes dynamiques qui utilise le nombre minimum d'opérations Node.insertBefore() pour réorganiser les nœuds DOM.
La réduction des opérations Node.insertBefore() est importante non seulement parce qu'elle invalide l'état DOM interne, mais aussi parce que chaque fois que l'un des nœuds DOM attachés au document est déplacé, il peut produire une notification MutationObserver. Et de nombreuses extensions populaires utilisent des observateurs de mutation pour observer le sous-arbre de document entier, de sorte que chaque opération insertBefore peut devenir assez coûteuse lorsqu'elle est utilisée en dehors des bacs de sable d'analyse comparative.
Les composants peuvent être avec état ou apatrides. Des composants avec état sont utilisés lorsque vous devez gérer l'état qui change dans le temps, tel que l'entrée des utilisateurs, les demandes de réseau ou les animations.
Les composants avec état sont déclarés avec la fonction component() . Il crée une fonction d'usine qui produit des nœuds de composants.
// `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" } ) ,
) ;Les composants avec état utilisent des fermetures JavaScript pour stocker l'état interne.
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 >
` ;
} ) ; Lorsque l'état interne est muté, il ne déclenche pas les mises à jour des composants automatiquement et il doit être invalidé manuellement avec la fonction invalidate() .
Il existe des API de haut niveau comme useState() ou useReducer() qui utilisent la fonction invalidate() de bas niveau dans les coulisses pour invalider automatiquement les composants lorsque l'état interne est muté.
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 > `
) ;
} ) ;Les composants apatrides en IVI ne sont que des fonctions JavaScript de base. Ils sont plus rapides et plus légers que les composants avec état, ce qui en fait un bon choix pour des composants simples et réutilisables qui n'ont pas d'état interne.
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 nœud racine est le nœud le plus haut dans un arbre avec état, à partir duquel tous les autres nœuds sont rendus. Il représente un point d'entrée pour l'algorithme de rendu IVI et stocke une position dans l'arbre DOM.
createRoot() createRoot crée un nœud racine qui utilise la file d'attente de microtasques pour la planification des mises à jour.
function createRoot (
parentElement : Element ,
nextNode : Node | null = null ,
) : Root ;parentElement - élément parent Dom.nextNode - Next Dom Node. dirtyCheck() dirtyCheck effectue l'algorithme de vérification sale dans un sous-arbre racine et met à jour tous les composants sales.
function dirtyCheck (
root : Root ,
forceUpdate : boolean = false ,
) : void ;root - Nœud racine.forceUpdate - Forcez tous les composants à mettre à jour, même lorsqu'ils utilisent des conseils d'optimisation pour réduire les mises à jour. update() update met à jour un sous-arbre racine avec une nouvelle représentation.
function update (
root : Root ,
v : VAny ,
forceUpdate : boolean = false ,
) : void ;root - Nœud racine.v - Nouvelle représentation.forceUpdate - Forcez tous les composants à mettre à jour, même lorsqu'ils utilisent des conseils d'optimisation pour réduire les mises à jour. unmount() unmount un montage de sous-arbre racine du DOM et déclenche des crochets de non-mèche dans les composants.
function unmount (
root : Root ,
detach : boolean ,
) : void ;root - Nœud racine.detach - détachez les nœuds Dom les plus supérieurs du sous-arbre Dom. defineRoot() defineRoot crée une usine de nœuds racine qui utilise un crochet OnRootInvalidated personnalisé.
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 - crochet OnRootInvalidated qui reçoit un nœud racine et un état personnalisé associé à ce nœud racine.component() component crée une usine qui produit des nœuds de composants.
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 - qui produit des fonctions de rendu de composants avec état.areEqual - Fonction facultative qui vérifie les propriétés d'entrée pour les modifications et est utilisée comme indice d'optimisation pour réduire les mises à jour inutiles lorsque les propriétés n'ont pas changé. Lorsque Root Subtree est mis à jour avec l'option forceUpdate , areEqual indice est ignoré et que tous les composants sont mis à jour.
getProps() getProps obtient des accessoires de composants actuels à partir de l'instance du composant.
function getProps = < P > ( component : Component < P > ) : P ;component - instance de composant. invalidate() invalidate le composant invalide et planifie une mise à jour.
function invalidate ( component : Component ) : void ;component - instance de composant. useUnmount()Ajoute un crochet non monté.
function useUnmount (
component : Component ,
hook : ( ) => void ,
) : void ;component - instance de composant.hook - Hook Unmount.useMemo() useMemo crée une fonction mémorisée.
function useMemo < T , U > (
areEqual : ( prev : T , next : T ) => boolean ,
fn : ( props : T ) => U ,
) : ( props : T ) => U ;areEqual - vérifie les propriétés d'entrée pour les modifications pour éviter les récomputations.fn - fonction à Memoize. useState() useState crée un état de composant réactif.
function useState < S > (
component : Component ,
state : S ,
) : [
get : ( ) => S ,
set : ( s : S ) => void ,
] ;component - instance de composant.state - état initial.Renvoie State Getter et State Setter Fonctions.
useReducer() useReducer crée un réducteur d'état de composant réactif.
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 - instance de composant.state - état initial.reducer - Fonction de réducteur d'état.Renvoie les fonctions State Getter et Action Dispatcher.
Les effets secondaires vous permettent de spécifier comment vos composants doivent se comporter avec des systèmes externes tels que les appels API impératifs, les manipulations de temporisation ou les interactions DOM directes.
Vous pouvez le considérer comme une combinaison de crochets de Lifecycles mount , update et unmount .
useEffect() useEffect crée un effet secondaire qui est exécuté immédiatement après que le nœud racine termine une mise à jour.
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 - instance de composant.effect - Effet Hook.areEqual - Fonction facultative qui vérifie les propriétés d'entrée pour les modifications et est utilisée pour contrôler lorsqu'un effet doit être mis à jour.Renvoie une fonction d'effet secondaire qui doit être invoquée dans une fonction de rendu.
useLayoutEffect() useLayoutEffect crée un effet secondaire qui est exécuté avant le cadre d'animation.
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 - instance de composant.effect - Effet Hook.areEqual - Fonction facultative qui vérifie les propriétés d'entrée pour les modifications et est utilisée pour contrôler lorsqu'un effet doit être mis à jour.Renvoie une fonction d'effet secondaire qui doit être invoquée dans une fonction de rendu.
useIdleEffect() useIdleEffect crée un effet secondaire qui est exécuté lorsque le navigateur est inactif.
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 - instance de composant.effect - Effet Hook.areEqual - Fonction facultative qui vérifie les propriétés d'entrée pour les modifications et est utilisée pour contrôler lorsqu'un effet doit être mis à jour.Renvoie une fonction d'effet secondaire qui doit être invoquée dans une fonction de rendu.
List() List crée des listes dynamiques.
function List < E , K > (
entries : E [ ] ,
getKey : ( entry : E , index : number ) => K ,
render : ( entry : E ) => VAny ,
) : VList ;entries - Données d'entrée.getKey - Fonction qui devrait renvoyer une clé unique pour chaque saisie de données.render - fonction qui rend une entrée.context() context crée des fonctions de Getter et de contexte de contexte.
function context = < T > ( ) : [
get : ( component : Component ) => T | undefined ,
provider : ( value : T , children : VAny ) => VContext < T > ,
] Renvoie une fonction get qui trouve la valeur de contexte la plus proche et une fonction provider qui crée des nœuds de contexte.
// 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 est une trappe d'évacuation qui permet l'extension de l'algorithme de rendu IVI.
type ElementDirective = < E extends Element > (
element : E ,
) => void ;eventDispatcher() eventDispatcher crée un répartiteur d'événements qui trouve le nœud Child Dom le plus proche et émet une CustomEvent avec 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 - Type d'événement.options - Options d'événements qui seront utilisées lorsque l'événement sera expédié.Le répartiteur d'événements invoque les gestionnaires d'événements de manière synchrone. Tous les gestionnaires d'événements sont invoqués avant le retour du répartiteur des événements.
findDOMNode() findDOMNode trouve l'enfant de nœud Dom le plus proche qui appartient à un sous-arbre de nœud avec état.
function findDOMNode < T extends Node | Text > (
node : SNode | null ,
) : T | null ;node - Node avec état. containsDOMElement() containsDOMElement vérifie si un nœud avec état contient des éléments DOM dans son sous-arbre.
function containsDOMElement (
node : SNode ,
element : Element ,
) : boolean ;node - Node avec état.element - élément DOM. hasDOMElement() hasDOMElement vérifie si un nœud avec état a un élément DOM comme enfant.
function hasDOMElement (
node : SNode ,
child : Element ,
) : boolean ;node - Node avec état.child - Dom.preventUpdates() preventUpdates est une fonction NOOP qui renvoie toujours la valeur true .
function preventUpdates < T > ( a : T , b : T ) : true ; strictEq() strictEq vérifie les valeurs de l'égalité avec un opérateur d'égalité strict === .
function strictEq < T > ( a : T , b : T ) : boolean ; shallowEq() shallowEq vérifie les objets avec un algorithme d'égalité peu profond et utilise un opérateur d'égalité strict pour vérifier les valeurs individuelles pour l'égalité.
function shallowEq < T extends object > ( a : T , b : T ) : boolean ; shallowEqArray() shallowEqArray vérifie les tableaux avec un algorithme d'égalité peu profond et utilise un opérateur d'égalité strict pour vérifier les valeurs individuelles pour l'égalité.
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 ( ) ,
) ; L'algorithme d'invalidation des composants est implémenté en marquant la composante comme sale et marquant tous ses nœuds parents avec un drapeau qu'ils ont un sous-arbre sale. Lorsque l'algorithme de marquage atteint un nœud racine, il invoque le crochet OnRootInvalidated() qui peut être utilisé pour implémenter un planificateur personnalisé.
Dirty .DirtySubtree .DirtySubtree , crochet OnRootInvalidated() invoqué.Dirty , les parents déjà marqués d'un drapeau DirtySubtree .Lorsque le planificateur décide de mettre à jour un nœud racine avec un sous-arbre sale, il démarre un algorithme de vérification sale. Cet algorithme va de haut en bas dans un ordre de droite à gauche, visitant tous les nœuds avec un drapeau de sous-arbre sale jusqu'à ce qu'il atteigne un composant sale et le met à jour.
DirtySubtree , commence à vérifier ses enfants.Dirty , déclenche une mise à jour.Dirty , déclenche une mise à jour. L'une des raisons pour lesquelles la bibliothèque de base est si petite est que l'algorithme de mise à jour est implémenté dans l'ordre RTL. L'algorithme qui effectue des mises à jour dans l'ordre RTL simplifie de nombreux problèmes complexes avec les mises à jour DOM. Le principal problème avec les mises à jour DOM est que lorsque nous commençons à mettre à jour une structure d'arbre Dom, nous devons avoir une référence à un parent et à un nœud DOM suivant, afin que nous puissions utiliser parent.insertBefore(newNode, nextNode) . Dans la plupart des cas, il est facile de récupérer un nœud DOM suivant, mais il y a des cas de bord comme lorsque nous avons deux expressions conditionnelles adjacentes et que l'un de leurs états est qu'il supprime complètement un nœud Dom de l'arbre, ou deux composants adjacents avec des conditionnels à leurs racines, etc.
La majorité des bibliothèques traitent de ces cas de bord en introduisant des nœuds DOM marqueurs (commentaire ou un nœud de texte vide). Par exemple, pour implémenter les expressions conditionnelles, nous pouvons ajouter un nœud de texte vide lorsque conditionnel ne rend aucun nœud DOM et lorsque la condition conditionnelle va dans un état lorsqu'il doit ajouter un nœud DOM, il utilisera un nœud de marqueur comme référence du nœud DOM suivant. L'algorithme de mise à jour RTL dans IVI n'utilise aucun nœud de marqueur.
L'algorithme RTL utilisé dans l'IVI facilite également la mise en œuvre des déplacements de nœud sans introduire de chemins de code, de fragments supplémentaires et à peu près tout ce qui implique la mise à jour d'une structure DOM.
Chaque site d'appel qui crée un modèle a une identité unique, donc même des modèles identiques créés à partir de différents sites d'appel ne pourront pas se diffuser les uns contre les autres.
function TemplateUniqueIdentity ( condition , text ) {
return ( condition )
? html `div ${ text } `
: html `div ${ text } ` ;
} Dans l'exemple ci-dessus, lorsque condition est modifiée, au lieu de mettre à jour le nœud de texte, l'algorithme de mise à jour remplacera l'élément DIV entier par un nouveau.
Certains cas d'utilisation nécessitent beaucoup de lectures fréquentes d'une variable réactive. Et chaque fois que cette variable change, elle affecte beaucoup de nœuds d'interface utilisateur, comme la commutation entre les thèmes clairs / sombres.
Au lieu de créer de nombreux abonnements à ces variables, il est recommandé d'utiliser des valeurs JavaScript simples et de remender le sous-arbre de l'interface utilisateur avec dirtyCheck(root, true) lorsque ces valeurs sont modifiées.
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 ( ) ) ; Le clonage du modèle est une optimisation qui est utilisée pour le clonage des modèles HTML avec une méthode Node.cloneNode() .
Par défaut, le clonage du modèle est activé pour tous les modèles. Mais parfois, il serait gaspillé de créer un modèle pour le clonage et instancier à partir de celui-ci lorsque ce modèle est rendu une seule fois.
Pour désactiver le clonage, le modèle doit avoir un commentaire de premier plan /* preventClone */ . Par exemple
const Example = ( ) => /* preventClone */ html `
< div class =" Title " > ${ text } </ div >
` ; Les modèles avec un seul élément qui n'ont pas de propriétés statiques seront créés avec document.createElement() .
html ` < div attr = ${ 0 } > ${ 1 } </ div > ` ;Par défaut, les gestionnaires d'événements (expressions de fonction Arrow) sont automatiquement hissées à la portée la plus externe.
const Example = component ( ( c ) => {
const [ count , setCount ] = useState ( c , 0 ) ;
return ( ) => html `
< div @click = ${ ( ) => { setCount ( count ( ) + 1 ) ; } } > ${ count ( ) } </ div >
` ;
} ) ;Après le hisser des gestionnaires d'événements, il sera transformé 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 >
` ;
} ) ; Pour désactiver les gestionnaires d'événements qui coulent, le modèle doit avoir un commentaire de premier plan /* preventHoist */ . Par exemple
const Example = component ( ( c ) => {
const [ count , setCount ] = useState ( c , 0 ) ;
return ( ) => /* preventHoist */ html `
< div @click = ${ ( ) => { setCount ( count ( ) + 1 ) ; } } > ${ count ( ) } </ div >
` ;
} ) ; Plusieurs annotations peuvent être déclarées en les séparant avec | Opérateur, par exemple /* preventClone | preventHoist */
Pour obtenir une estimation approximative de l'utilisation de la mémoire, il est important de comprendre les structures de données internes.
Dans la description ci-dessous, nous allons calculer l'utilisation de la mémoire dans des moteurs à base de chrome avec compression du pointeur dans V8.
L'arbre d'interface utilisateur est mis en œuvre avec un arbre SNode et un arbre sans état VAny .
L'arbre sans état a une structure de données 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 [ ] ,
} Pour chaque nœud sans état VAny il y a un nœud SNode base snode qui a une 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 > ;Ces structures de données ont été soigneusement conçues pour avoir de petites frais générales de mémoire et éviter beaucoup de sites d'appels polymorphes / mégamorphiques qui accèdent à ces structures de données.
Pour comprendre pourquoi les sites d'appels monomorphes sont importants pour les performances, il est recommandé de lire un excellent article sur ce sujet: "Qu'est-ce qui se passe avec le monomorphisme?".
Les modèles sont précompilés dans une partie statique qui est stockée dans un objet de TemplateDescriptor et un tableau d'expressions dynamiques.
const Example = ( attr , child ) => html `div :attr= ${ attr } span ${ child } ` ;Est compilé dans:
// _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 > ;Le compilateur de modèle n'élimine pas seulement l'étape de compilation pendant l'exécution, il hisse également les attributs statiques et les écouteurs d'événements, les opodes dédupliqués, les chaînes et les fonctions d'usine de modèle. Par exemple
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 >
` ;Générera deux modèles différents avec des structures de données partagées:
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 ] ) ;Très souvent, les opcodes qui sont utilisés à des fins différentes (accessoires, enfants, état) vont avoir des valeurs similaires, donc lorsque les opcodes sont dédupliqués, ils sont traités comme de simples tableaux avec des entiers qui peuvent être utilisés à des fins différentes.
Les trassages partagés (clés d'attribut, noms d'événements, etc.) sont dédupliqués en un tableau ( __IVI_STRINGS__ ) qui est partagé entre tous les modèles.
IVI est conçu comme une solution intégrée, afin qu'il puisse être intégré dans des cadres ou des composants Web existants. Le nœud racine de base instancié avec createRoot() utilise la file d'attente Microtask pour planifier les mises à jour. Les nœuds racine avec un algorithme de planification personnalisé peuvent être créés en définissant de nouvelles usines racine avec la fonction 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 > ;Par exemple, pour supprimer tout lot et mettre à jour immédiatement le sous-arbre racine lorsqu'il est invalidé, nous pouvons définir le nœud racine suivant:
import { defineRoot } from "ivi" ;
const createSyncRoot = defineRoot ( ( root ) => {
// Immediately triggers dirty checking.
dirtyCheck ( root ) ;
} ) ; requestAnimationFrame() pour la planification des mises à jour de l'interface utilisateur L'algorithme de planification avec lot rAF a des buts potentiels avec des conditions de course.
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 ( ) ,
) ; Dans cet exemple, si l'utilisateur tape très rapidement et pousse un bouton [enter] , il est possible d'obtenir un ordre d'exécution comme ceci:
0 dans <input> .onChange() est déclenché, state.valid passe à un false état.[enter] .<input type="submit" .disabled={false} />rAF est déclenché, le bouton Soumettre va à l'état désactivé. Le moyen le plus simple d'éviter des problèmes comme celui-ci est d'utiliser des microtasses pour les lots. Mais si vous voulez vraiment ajouter la planification rAF , il est possible de résoudre des problèmes comme celui-ci en introduisant certaines primitives de synchronisation:
import { uiReady } from "my-custom-scheduler" ;
const onSubmit = async ( ev ) => {
await uiReady ( ) ;
submit ( ) ;
} ; Le runtime IVI ne dépend pas des bibliothèques externes.
IVI Dev Tools a un ensemble minimal de dépendances:
@rollup/pluginutils est utilisé dans un plugin rollup @ivi/rollup-plugin pour filtrer les modules. Mit