IVIは、軽量埋め込み可能な宣言的Web UIライブラリです。
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 ( ) ,
) ;上記の事前に拡張された例のサイズはわずか2.7kbです(Minified+Brotli)。宣言的なUIレンダリングのランタイム全体が含まれます。プリコンパイルされたテンプレートは、コードサイズとコールドスタートのパフォーマンスに最適化されています。
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)IVIテンプレートは、事前コンパイルなしで機能しますが、パフォーマンスを改善し、コードサイズを削減するために事前コンパイルを使用することを強くお勧めします。
"@ivi/vite-plugin"パッケージはViteプラグインを提供します。
// vite.config.mjs
import { defineConfig } from "vite" ;
import { ivi } from "@ivi/vite-plugin" ;
export default defineConfig ( {
plugins : [ ivi ( ) ] ,
} ) ;"@ivi/rollup-plugin"パッケージは、ロールアッププラグインを提供します。
// rollup.config.mjs
import { ivi } from "@ivi/rollup-plugin" ;
export default {
input : "src/main.js" ,
output : {
file : "bundle.js" ,
} ,
plugins : [ ivi ( ) ]
} ; IVIテンプレート言語には、DOMプロパティ、イベント、および空白除去の追加構文を備えたHTMLのような構文があります。
html 、HTMLelementノードを備えたテンプレートを作成します。svg 、SVGElementノードを備えたテンプレートを作成します。 import { html } from "ivi" ;
const Example = component ( ( c ) => {
// ...
return ( ) => html `
< div class =" app " >
< div > ${ count ( ) } </ div >
< button @click = ${ inc } > Increment </ button >
</ div >
` ;
} ) ;テンプレートには複数のルートノードがあります。
html `
< div > </ div >
${ expr }
text
< div > </ div >
`子どものいない要素は、A /> Syntaxで自己閉鎖できます。
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文字は、ニューラインの周りのすべての空白を削除するのを防ぎます。 < div >
< b > 1 </ b >
v item left
< div > < div > < b > 1 </ b > item left </ div >IVIテンプレートでは、式と呼ばれる動的なコンテンツを含めることができます。式は、テンプレートがレンダリングされたときに評価されるJavaScriptコードの一部です。その時点で式が生成する価値が何であれ、最終レンダリングされたテンプレートに含まれます。
html `
< div attr = ${ attributeValueExpr } >
${ childExpr }
</ div > ` ;IVIテンプレート言語は、DOMプロパティ、イベントなどで動作するための追加の構文をサポートしています。
<div name="value" /> - static属性。<div name /> - 静的属性。<div name=${expr} /> - 動的属性element.setAttribute(name, expr) 。<div .name=${expr} /> - プロパティelement[name] = expr 。<div *name=${expr} /> - プロパティelement[name] = expr 、dom値に対してdiffs。<div ~name="value" /> - static style <div style="name:value;"> 。<div ~name=${expr} /> - 動的スタイルelement.style.setProperty(name, expr) 。<div @name=${expr} /> - event element.addEventListener(name, expr) 。<div ${directive} /> - クライアント側の要素directive(element) 。<div .textContent=${expr} /> - テキストコンテンツ。 <div name="value" /> - 値を持つ静的属性<div name="value"> 。<div name /> - 値なしの静的属性<div name> 。<div name=${expr} /> - 動的属性element.setAttribute(name, expr) 。 DOM属性はElement.setAttribute(..)で割り当てられます。
動的属性にundefined 、 null 、またはfalse値がある場合、 Element.removeAttribute(..)メソッドを使用してdom elementから削除されます。
<div .name=${expr} /> - プロパティelement[name] = expr 。<div *name=${expr} /> - プロパティelement[name] = expr 、dom値に対してdiffs。プロパティは、割り当て演算子Element.name = value 。
DOM値を拡張することは、不要なinputイベントのトリガーを避けるために<input>値を使用する場合に使用されます。
<div ~name="value" /> - static style <div style="value"> 。<div ~name=${expr} /> - 動的スタイルelement.style.setProperty(name, expr) 。静的スタイルは:style="value"属性と自動的にマージされます。
動的スタイルにはCSSStyleDeclaration.setProperty(..)メソッドが付属されています。
スタイルにundefined 、 null 、またはfalse値がある場合、 CSSStyleDeclaration.removeProperty(..)メソッドで削除されます。
<div @name=${expr} /> - event element.addEventListener(name, expr) 。イベントにはEventTarget.addEventListener(..)メソッドが付属されています。
イベントにundefined 、 null 、またはfalse値がある場合、 EventTarget.removeEventListener(..)メソッドで削除されます。
<div .textContent=${expr} /> - テキストコンテンツelement.textContent = expr 。テキストコンテンツプロパティは、テキストの子を持つ要素のメモリ消費をわずかに削減する最適化として使用できます。 Node.textContentプロパティを使用してテキストノードが作成され、テキストノードに関連付けられたステートフルノードはありません。
テキストコンテンツ値には、 undefined 、 null 、 false 、 string 、またはnumberタイプが必要です。
<div ${directive} /> - 要素directive(element) 。ディレクティブは、テンプレートが更新され、指令に関連付けられたDOM要素を受信するたびに呼び出される関数です。
type ElementDirective = < E extends Element > (
element : E ,
) => void ;ディレクティブ関数は、テンプレートが異なる関数で作成された場合にのみ呼び出されるため、同じ関数を再利用する場合、Callbackを作成したDOM要素として使用できます。
const Example = component ( ( c ) => {
const onCreated = ( innerElement ) => {
// ..
} ;
return ( ) => html `
< div >
< div class =" Inner " ${ onCreated } />
</ div >
` ;
} ) ;ディレクティブは、単純なDOMが作成したコールバックとしてだけでなく、ステートフルディレクティブとしても使用できます。例えば
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 ) } />
` ;
} ) ;テンプレートで通常のJavaScript式を使用できます。つまり、条件付き演算子、関数呼び出し、およびランタイム条件に基づいて動的コンテンツを生成するためにステートメントを切り替えるかどうかのJavaScriptコントロールフローコンストラクトを使用できます。
これは、アプリケーションで何が起こっているかに基づいて条件付きで異なるコンテンツをレンダリングする複雑なロジックを備えたテンプレートを作成できることを意味します。テンプレート式を互いにネストして、より複雑なテンプレートを構築することができます。また、テンプレートの結果を変数に保存して、コードで後で使用できます。
const Example = component ( ( c ) => {
// ...
return ( show ) => html `
< div >
${ show && html ` < span > Show </ span > ` }
</ div >
` ;
} ) ;式がHTML要素の子位置で使用され、配列を返す場合、IVIはその配列内のすべてのアイテムを個別のノードとしてレンダリングします。
const Example = ( ) => html `
< div >
${ [
"Text Node 1" ,
"Text Node 2" ,
] }
</ div >
` ;IVIを使用すると、コンポーネントは要素の配列をルートノードとして返すことができます。これは、コンポーネントが1つではなく複数のトップレベル要素を返すことができることを意味します。
たとえば、コンポーネントは、リストを構成する<li>要素の配列を返すことができます。このコンポーネントがレンダリングされると、IVIは<li>要素の配列を、単一のルート要素と同様に、一連のトップレベル要素として扱います。
この機能は、入力に応じて動的な数のトップレベル要素を生成するコンポーネントを作成できるため、複雑なUIコンポーネントを構築するときに柔軟性を高めます。
const Example = component ( ( c ) => {
return ( entries ) => entries . map ( ( e ) => html `
< li > ${ e } </ li >
` ) ;
) ;
// Example([1, 2, 3])配列が更新されると、ステートレスツリーノードは、配列内の位置によってステートフルなノードにマッピングされます。
アレイに「穴」値( null 、 undefinedまたはfalse )を返す条件付き式が含まれている場合、穴はステートフルツリーのスロットを占有し、すべてのノードがステートフルなノードにマッピングされます。
[
conditional ? "text" : null ,
StatefulComponent ( ) ,
]上記の例では、 conditional式がテキストから「穴」に移動し、その逆になると、 StatefulComponentその内部状態を維持します。
アレイのサイズが大きくなったり縮小したりすると、アレイの最後にステートフルなノードが作成または削除されます。
IVIでは、一連のデータをループして要素のリストを返すList()関数を使用してアイテムのリストをレンダリングできます。ただし、リストが更新された場合、レンダリングされたアイテムをステートフルビューに正しくマッピングすることが重要です。これは、ユーザーアクションまたは外部イベントの結果として変更される可能性のある内部状態を持つコンポーネントとしてアイテムがレンダリングされている場合、同じコンポーネントインスタンスにマッピングする必要があることを意味します。
動的リストをレンダリングするために、IVIは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 ;リスト内の各アイテムを独自に識別する一連のキーを備えた動的リストを作成します。リストが更新されると、IVIはキーを使用してアイテムをステートフルなノードにマッピングします。
動的リストをレンダリングするときは、常に一意の識別子をキーとして使用する必要があることに注意することが重要です。これにより、IVIはリスト内の各要素を識別し、エラーのレンダリングを回避できます。インデックスまたはランダム値をキーとして使用する場合、IVIはリスト内の正しい要素を識別できない場合があり、エラーを引き起こす可能性があります。
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は、DOMノードを再配置するためにNode.insertBefore()操作の最小数を使用する動的リストに最適なアルゴリズムを使用しています。
Node.insertBefore()操作の削減は、内部Dom状態を無効にするだけでなく、ドキュメントに接続されたDOMノードの1つが移動されるたびに、MutationObserver通知を生成する可能性があるために重要です。また、多くの一般的な拡張機能は、突然変異オブザーバーを使用してドキュメントサブツリー全体を観察しているため、ベンチマークのサンドボックスの外で使用すると、各insertBefore操作が非常にコストがかかる可能性があります。
コンポーネントは、ステートフルまたはステートレスのいずれかです。ステートフルコンポーネントは、ユーザー入力、ネットワークリクエスト、アニメーションなど、時間の経過とともに変化する状態を管理する必要がある場合に使用されます。
ステートフルコンポーネントはcomponent()関数で宣言されます。コンポーネントノードを生成する工場関数を作成します。
// `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" } ) ,
) ;ステートフルコンポーネントは、JavaScriptクロージャーを使用して内部状態を保存しています。
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 >
` ;
} ) ;内部状態が変異している場合、コンポーネントの更新を自動的にトリガーせず、 invalidate()関数で手動で無効にする必要があります。
useState()やuseReducer()のような高レベルのAPIがあり、舞台裏で低レベルのinvalidate()関数を使用して、内部状態が変異したときにコンポーネントを自動的に無効にします。
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 > `
) ;
} ) ;IVIのステートレスコンポーネントは、基本的なJavaScript関数にすぎません。それらは、ステートフルコンポーネントよりも高速で軽量であるため、内部状態のないシンプルで再利用可能なコンポーネントに適しています。
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 ;ルートノードは、ステートフルツリーの最上位ノードであり、そこから他のすべてのノードがレンダリングされます。これは、IVIレンダリングアルゴリズムのエントリポイントを表し、DOMツリーに位置を保存します。
createRoot() createRoot 、更新のスケジューリングにMicrotaskキューを使用するルートノードを作成します。
function createRoot (
parentElement : Element ,
nextNode : Node | null = null ,
) : Root ;parentElement -Parent Dom Element。nextNode -Next Domノード。 dirtyCheck() dirtyCheck 、ルートサブツリーでDirty Checkingアルゴリズムを実行し、すべてのダーティコンポーネントを更新します。
function dirtyCheck (
root : Root ,
forceUpdate : boolean = false ,
) : void ;root - ルートノード。forceUpdate最適化ヒントを使用して更新を減らす場合でも、すべてのコンポーネントに更新を強制します。 update() update新しい表現でルートサブツリーを更新します。
function update (
root : Root ,
v : VAny ,
forceUpdate : boolean = false ,
) : void ;root - ルートノード。v新しい表現。forceUpdate最適化ヒントを使用して更新を減らす場合でも、すべてのコンポーネントに更新を強制します。 unmount() unmount 、DOMからルートサブツリーをアンマウントし、コンポーネントのフックをアンマウントするトリガーをトリガーします。
function unmount (
root : Root ,
detach : boolean ,
) : void ;root - ルートノード。detach - 最上部のDOMノードをDOMサブツリーから取り外します。 defineRoot() defineRoot 、カスタム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そのルートノードに関連付けられたルートノードとカスタム状態を受信するOnRootInvalidatedフック。component() component 、コンポーネントノードを生成する工場を作成します。
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 - ステートフルコンポーネントレンダリング機能を生成する機能。areEqualの関数。 Root SubtreeがforceUpdateオプションで更新されると、 areEqual Hintは無視され、すべてのコンポーネントが更新されます。
getProps() getProps 、コンポーネントインスタンスから現在のコンポーネントプロップを取得します。
function getProps = < P > ( component : Component < P > ) : P ;component - コンポーネントインスタンス。 invalidate() Invalidateコンポーネントをinvalidate 、更新をスケジュールします。
function invalidate ( component : Component ) : void ;component - コンポーネントインスタンス。 useUnmount()アンマウントフックを追加します。
function useUnmount (
component : Component ,
hook : ( ) => void ,
) : void ;component - コンポーネントインスタンス。hook - アンマウントフック。useMemo() useMemo 、メモ化された関数を作成します。
function useMemo < T , U > (
areEqual : ( prev : T , next : T ) => boolean ,
fn : ( props : T ) => U ,
) : ( props : T ) => U ;areEqual 。fnメモに関数。 useState() useState 、リアクティブコンポーネント状態を作成します。
function useState < S > (
component : Component ,
state : S ,
) : [
get : ( ) => S ,
set : ( s : S ) => void ,
] ;component - コンポーネントインスタンス。state - 初期状態。State GetterおよびState Setter機能を返します。
useReducer() useReducer 、リアクティブコンポーネントの状態還元剤を作成します。
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 - コンポーネントインスタンス。state - 初期状態。reducer - 状態還元剤関数。State Getter and Action Dispatcher機能を返します。
副作用を使用すると、コンポーネントが必須API呼び出し、タイマー操作、直接DOM相互作用などの外部システムで動作する方法を指定できます。
mount 、 update 、 unmountライフサイクルフックの組み合わせと考えることができます。
useEffect() useEffectルートノードが更新を終了した直後に実行される副作用を作成します。
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 - コンポーネントインスタンス。effect - エフェクトフック。areEqual変化の入力プロパティをチェックし、効果を更新するときに制御するために使用されるオプションの関数。レンダリング関数で呼び出す必要がある副作用関数を返します。
useLayoutEffect() useLayoutEffect 、アニメーションフレームの前に実行される副作用を作成します。
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 - コンポーネントインスタンス。effect - エフェクトフック。areEqual変化の入力プロパティをチェックし、効果を更新するときに制御するために使用されるオプションの関数。レンダリング関数で呼び出す必要がある副作用関数を返します。
useIdleEffect() useIdleEffectアイドル状態にあるときに実行される副作用を作成します。
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 - コンポーネントインスタンス。effect - エフェクトフック。areEqual変化の入力プロパティをチェックし、効果を更新するときに制御するために使用されるオプションの関数。レンダリング関数で呼び出す必要がある副作用関数を返します。
List() List動的リストを作成します。
function List < E , K > (
entries : E [ ] ,
getKey : ( entry : E , index : number ) => K ,
render : ( entry : E ) => VAny ,
) : VList ;entries - 入力データ。getKeyデータエントリごとに一意のキーを返す必要がある関数。render - エントリをレンダリングする関数。context() context 、コンテキストゲッターおよびコンテキストプロバイダー機能を作成します。
function context = < T > ( ) : [
get : ( component : Component ) => T | undefined ,
provider : ( value : T , children : VAny ) => VContext < T > ,
]最も近いコンテキスト値を見つけるget関数と、コンテキストノードを作成するprovider関数を返します。
// 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は、IVIレンダリングアルゴリズムを拡張できるエスケープハッチです。
type ElementDirective = < E extends Element > (
element : E ,
) => void ;eventDispatcher() eventDispatcher 、最も近いChild Domノードを見つけ、 EventTarget.dispatchEvent()メソッドでCustomEventを発するイベントディスパッチャーを作成します。
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イベントタイプ。options - イベントが発送されるときに使用されるイベントオプション。イベントディスパッチャーは、イベントハンドラーを同期して呼び出します。すべてのイベントハンドラーは、イベントディスパッチャーが返す前に呼び出されます。
findDOMNode() findDOMNode 、ステートフルなノードサブツリーに属する最も近いDOMノードチャイルドを見つけます。
function findDOMNode < T extends Node | Text > (
node : SNode | null ,
) : T | null ;node - ステートフルなノード。 containsDOMElement() containsDOMElement Check StatefulノードにサブツリーにDOM要素が含まれているかどうか。
function containsDOMElement (
node : SNode ,
element : Element ,
) : boolean ;node - ステートフルなノード。element -DOM要素。 hasDOMElement() hasDOMElement 、ステートフルノードが子供のようにDOM要素を持っているかどうかを確認します。
function hasDOMElement (
node : SNode ,
child : Element ,
) : boolean ;node - ステートフルなノード。child - dom要素。preventUpdates() preventUpdates 、常にtrue値を返すNOOP関数です。
function preventUpdates < T > ( a : T , b : T ) : true ; strictEq() strictEq 、厳密な平等オペレーター===で平等の値をチェックします。
function strictEq < T > ( a : T , b : T ) : boolean ; shallowEq() shallowEq 、浅い平等アルゴリズムを持つオブジェクトをチェックし、Strict Equalityオペレーターを使用して個々の値を確認します。
function shallowEq < T extends object > ( a : T , b : T ) : boolean ; shallowEqArray() shallowEqArray 、浅い平等アルゴリズムを使用して配列をチェックし、Strict Equalityオペレーターを使用して個々の値を等価性を確認します。
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 ( ) ,
) ; コンポーネント無効化アルゴリズムは、コンポーネントを汚れたものとしてマークし、すべての親ノードを汚れたサブツリーを持っているフラグでマークすることにより実装されます。アルゴリズムをマークすると、ルートノードに到達すると、カスタムスケジューラを実装するために使用できるOnRootInvalidated()フックが呼び出されます。
Dirty旗でマークされています。DirtySubtreeフラグでマークされたノード。DirtySubtreeフラグでマークされたルートノード、 OnRootInvalidated()フックが呼び出されました。Dirty旗でマークされており、両親はすでにDirtySubtree旗でマークされています。スケジューラが汚れたサブツリーを使用してルートノードを更新することを決定すると、汚れたチェックアルゴリズムを開始します。このアルゴリズムは、右から左への順序でトップダウンし、汚れたサブツリーフラグですべてのノードにアクセスして、汚れたコンポーネントに到達して更新します。
DirtySubtree Flagを使用したノードは、子供のチェックを開始します。Dirty Flagのコンポーネントは、更新をトリガーします。Dirty Flagのコンポーネントは、更新をトリガーします。コアライブラリが非常に小さい理由の1つは、更新アルゴリズムがRTL順序で実装されているためです。 RTL注文で更新を実行するアルゴリズムは、DOMの更新に関する多くの複雑な問題を簡素化します。 DOMの更新の主な問題は、DOMツリー構造の更新を開始するときに、親と次のDOMノードへの参照が必要であることです。これにより、 parent.insertBefore(newNode, nextNode)を使用できます。ほとんどの場合、次のDOMノードを簡単に取得できますが、2つの隣接する条件付き式がある場合のようなエッジケースがあり、その状態の1つは、ツリーからDOMノードを完全に削除すること、または根などに条件付きの2つの隣接するコンポーネントを完全に削除することです。
ライブラリの大部分は、マーカーDOMノード(コメントまたは空のテキストノード)を導入することにより、このエッジケースを扱っています。たとえば、条件付き式を実装するには、条件付きで空のテキストノードを追加しない場合、DOMノードをレンダリングしない場合、および条件がDOMノードを追加する必要がある場合に状態に入ると、次のDOMノードリファレンスとしてマーカーノードを使用します。 IVIのRTL更新アルゴリズムは、マーカーノードを使用しません。
IVIで使用されるRTLアルゴリズムは、DOM構造の更新を含む追加のコードパス、フラグメント、およびほとんどすべてを導入することなく、ノード変位を実装するのがはるかに簡単になります。
テンプレートを作成する各コールサイトには一意のアイデンティティがあるため、異なるコールサイトから作成された同一のテンプレートでさえ、互いに異なることができません。
function TemplateUniqueIdentity ( condition , text ) {
return ( condition )
? html `div ${ text } `
: html `div ${ text } ` ;
}上記の例では、 conditionが変更された場合、テキストノードを更新する代わりに、更新アルゴリズムはDiv要素全体を新しい要素に置き換えます。
リアクティブ変数から多くの頻繁な読み取りを必要とするいくつかのユースケースがあります。また、この変数が変更されるたびに、光/暗いテーマを切り替えるなど、多くのUIノードに影響します。
この変数に多くのサブスクリプションを作成する代わりに、この値が変更されたときにdirtyCheck(root, true)でuiサブツリー全体を使用して、単純なJavaScript値を使用することをお勧めします。
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 ( ) ) ;テンプレートクローニングはNode.cloneNode()メソッドを使用してHTMLテンプレートのクローニングに使用される最適化です。
デフォルトでは、すべてのテンプレートに対してテンプレートクローニングが有効になっています。しかし、このテンプレートが一度だけレンダリングされたときに、クローン用のテンプレートを作成し、そこからインスタンス化することは無駄になることがあります。
クローニングを無効にするには、テンプレートには主要なコメント/* preventClone */が必要です。例えば
const Example = ( ) => /* preventClone */ html `
< div class =" Title " > ${ text } </ div >
` ;静的プロパティを持たない要素が1つだけのテンプレートはdocument.createElement()で作成されます。
html ` < div attr = ${ 0 } > ${ 1 } </ div > ` ;デフォルトでは、イベントハンドラー(矢印関数式)は、最も外側のスコープに自動的に巻き上げられます。
const Example = component ( ( c ) => {
const [ count , setCount ] = useState ( c , 0 ) ;
return ( ) => html `
< div @click = ${ ( ) => { setCount ( count ( ) + 1 ) ; } } > ${ count ( ) } </ div >
` ;
} ) ;イベントハンドラーが巻き上げられた後、それは次のように変換されます:
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 >
` ;
} ) ;イベントハンドラーを巻き上げて無効にするには、テンプレートには主要なコメント/* preventHoist */を持つ必要があります。例えば
const Example = component ( ( c ) => {
const [ count , setCount ] = useState ( c , 0 ) ;
return ( ) => /* preventHoist */ html `
< div @click = ${ ( ) => { setCount ( count ( ) + 1 ) ; } } > ${ count ( ) } </ div >
` ;
} ) ;複数の注釈は、それらを分離することで宣言できます|オペレーター、Eg /* preventClone | preventHoist */
メモリ使用量の大まかな推定を取得するには、内部データ構造を理解することが重要です。
以下の説明では、V8のポインター圧縮を伴うクロムベースのエンジンでメモリ使用量を計算します。
UIツリーは、ステートフルなツリースSNodeと不変のステートレスツリーVAnyで実装されています。
ステートレスツリーには、単純なデータ構造があります。
// 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 [ ] ,
}各ステートレスノードVAnyには、インターフェイスを持つステートフルなノードSNodeがあります。
// 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 > ;このデータ構造は、小さなメモリオーバーヘンを持つように慎重に設計されており、このデータ構造にアクセスする多型/巨大なコールサイトを多く回避します。
単量体のコールサイトがパフォーマンスに重要である理由を理解するには、このトピックに関する素晴らしい記事を読むことをお勧めします。
テンプレートは、 TemplateDescriptorオブジェクトと動的式の配列に保存されている静的部分に事前拡張されます。
const Example = ( attr , child ) => html `div :attr= ${ attr } span ${ child } ` ;にコンパイルされます:
// _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 > ;テンプレートコンパイラは、ランタイム中のコンピレーションステップを排除するだけでなく、静的属性とイベントリスナーを掲げたり、オペコード、文字列、テンプレートの工場関数を推測したりします。例えば
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 >
` ;共有データ構造を使用して2つの異なるテンプレートを生成します。
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 ] ) ;多くの場合、さまざまな目的(小道具、子、状態)に使用されるオペコードは同様の値を持つため、オプコードを重複させると、異なる目的で使用できる整数を備えた単純な配列として扱われます。
共有ストリング(属性キー、イベント名など)は、すべてのテンプレート間で共有される1つの配列( __IVI_STRINGS__ )に重複しています。
IVIは、既存のフレームワークまたはWebコンポーネントに統合できるように、組み込み可能なソリューションとして設計されています。 createRoot()関数がインスタンス化された基本的なルートノードは、Microtaskキューを使用して更新をスケジュールしています。カスタムスケジューリングアルゴリズムを備えたルートノードは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 > ;例として、バッチを削除し、無効化されたらすぐにルートサブツリーを更新するために、次のルートノードを定義できます。
import { defineRoot } from "ivi" ;
const createSyncRoot = defineRoot ( ( root ) => {
// Immediately triggers dirty checking.
dirtyCheck ( root ) ;
} ) ; requestAnimationFrame()を使用しますrAFバッチを使用したスケジューリングアルゴリズムには、レース条件のある潜在的なフットガンがあります。
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 ( ) ,
) ;この例では、ユーザーが非常に速く入力して[enter]ボタンを押すと、次のような実行注文を取得することができます。
0に<input> 。onChange()イベントハンドラーがトリガーされ、 state.validスイッチがfalse状態になります。[enter]ボタンを押します。<input type="submit" .disabled={false} />rAFイベントがトリガーされ、送信ボタンが無効状態になります。このような問題を回避する最も簡単な方法は、バッチにマイクロタスクを使用することです。しかし、 rAFスケジューリングを本当に追加したい場合は、同期プリミティブを導入することでこのような問題を解決することができます。
import { uiReady } from "my-custom-scheduler" ;
const onSubmit = async ( ev ) => {
await uiReady ( ) ;
submit ( ) ;
} ; IVIランタイムは、外部ライブラリに依存しません。
IVI開発ツールには、依存関係の最小限があります。
@rollup/pluginutils 、ロールアッププラグイン@ivi/rollup-pluginで使用され、モジュールを除外します。 mit