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(缩小+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模板语言具有类似于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 />语法自我封闭。
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" /> - 静态属性。<div name /> - 静态属性。<div name=${expr} /> - 动态属性element.setAttribute(name, expr) 。<div .name=${expr} /> - 属性element[name] = expr 。<div *name=${expr} /> - 属性element[name] = expr ,diff a dom value。<div ~name="value" /> - 静态样式<div style="name:value;"> 。<div ~name=${expr} /> - 动态样式element.style.setProperty(name, expr) 。<div @name=${expr} /> - 事件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元素中删除。
<div .name=${expr} /> - 属性element[name] = expr 。<div *name=${expr} /> - 属性element[name] = expr ,diff a dom value。属性分配了分配运算符Element.name = value 。
当我们使用<input>值以避免触发不必要的input事件时,与DOM值相差很有用。
<div ~name="value" /> - 静态样式<div style="value"> 。<div ~name=${expr} /> - 动态样式element.style.setProperty(name, expr) 。静态样式自动合并:style="value"属性。
使用CSSStyleDeclaration.setProperty(..)方法分配动态样式。
如果样式具有undefined , null或false值,则将使用CSSStyleDeclaration.removeProperty(..)方法将其删除。
<div @name=${expr} /> - 事件element.addEventListener(name, expr) 。事件是用EventTarget.addEventListener(..)方法分配的。
当事件具有undefined null或false值时,将使用EventTarget.removeEventListener(..)方法将其删除。
<div .textContent=${expr} /> - text content element.textContent = expr 。文本内容属性可以用作优化,可稍微降低带有文本孩子的元素的内存消耗。它将创建一个带有Node.textContent属性的文本节点,并且不会具有与文本节点相关的任何状态节点。
文本内容值应具有undefined , null , false , string或number类型。
<div ${directive} /> - 元素指令directive(element) 。指令是每个时间模板都会更新并接收与指令关联的DOM元素的函数:
type ElementDirective = < E extends Element > (
element : E ,
) => void ;只有在使用不同函数创建模板时才调用指令函数,因此,如果我们要重复使用相同的函数,则可以用作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控制流构造,例如条件运算符,功能调用以及IF或Switch语句以基于运行时条件生成动态内容。
这意味着您可以根据应用程序中发生的情况来创建具有复杂逻辑的模板,从而有条件地呈现不同的内容。您可以在彼此之间嵌套模板表达式以构建更复杂的模板,并且可以将模板的结果存储在变量中以稍后在代码中使用它们。
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允许组件返回元素作为根节点的数组。这意味着组件可以返回多个顶级元素,而不仅仅是一个元素。
例如,一个组件可以返回构成列表的<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正在使用最佳算法对动态列表,该列表使用最小数量的Node.insertBefore()操作来重新排列DOM节点。
减少Node.insertBefore()操作很重要,不仅是因为它使内部DOM状态无效,而且还因为每次移动附加到文档的DOM节点之一都会产生突变处理器通知。许多流行的扩展程序正在使用突变观察者观察整个文档子树,因此,当操作在基准沙盒外面使用时,每个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() ,例如Usestate()或用户educer 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 - 父元元素。nextNode下一个DOM节点。 dirtyCheck() dirtyCheck在根子树中执行肮脏的检查算法,并更新所有肮脏的组件。
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 Hook。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可选函数,该功能检查输入属性是否更改,并用作优化提示,以减少属性不变时不必要的更新。当使用forceUpdate选项更新root子树时,忽略了areEqual提示,并且所有组件均已更新。
getProps() getProps从组件实例获取当前组件道具。
function getProps = < P > ( component : Component < P > ) : P ;component - 组件实例。 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 - 初始状态。返回状态getter和状态设定器功能。
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 - 状态还原功能。返回状态Getter和Action调度程序功能。
副作用使您可以指定组件应如何使用外部系统(例如命令式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创建上下文getter和上下文提供商功能。
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创建了一个事件调度程序,该事件调度程序可以找到最接近的子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检查状态节点是否包含其子树中的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用浅层平等算法检查对象,并使用严格的平等操作员检查单个值以保持平等。
function shallowEq < T extends object > ( a : T , b : T ) : boolean ; shallowEqArray() shallowEqArray使用浅层平等算法检查数组,并使用严格的平等操作员来检查单个值以保持平等。
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 Flag。当调度程序决定使用肮脏的子树更新根节点时,它会启动肮脏的检查算法。该算法以左至左的订单上自上而下,访问所有带有肮脏子树标志的节点,直到它达到肮脏的组件并对其进行更新。
DirtySubtree旗的节点开始检查其孩子。Dirty标志的组件,触发更新。Dirty标志的组件,触发更新。核心库如此小的原因之一是因为更新算法是按RTL顺序实现的。在RTL顺序中执行更新的算法简化了DOM更新的许多复杂问题。 DOM更新的主要问题是,当我们开始更新DOM树结构时,我们需要对父级和下一个DOM节点进行引用,以便我们可以使用parent.insertBefore(newNode, nextNode) 。在大多数情况下,很容易检索下一个DOM节点,但是在某些情况下,例如当我们有两个相邻的条件表达式和它们的一个状态时,它完全从树中删除了一个DOM节点,或两个相邻的组件,其根部有条件,等等。
大多数库是通过引入标记DOM节点(注释或空文本节点)来处理这种边缘情况。例如,为了实现条件表达式,我们可以在条件不会呈现任何DOM节点时添加一个空文本节点,并且当有条件添加DOM节点时,有条件地进入状态时,它将使用标记节点作为下一个DOM节点引用。 IVI中的RTL更新算法不使用任何标记节点。
IVI中使用的RTL算法也使实现节点位移的情况变得更加容易,而无需引入任何其他代码路径,片段和几乎所有涉及更新DOM结构的所有内容。
创建模板的每个呼叫地点都有唯一的身份,因此即使是从不同呼叫点创建的相同模板也无法相互差异。
function TemplateUniqueIdentity ( condition , text ) {
return ( condition )
? html `div ${ text } `
: html `div ${ text } ` ;
}在上面的示例中,当更改condition时,而不是更新文本节点,更新算法将用新的DIV元素替换整个DIV元素。
有些用例需要从反应性变量中进行大量频繁读取。每当此变量变化时,它会影响许多UI节点,例如在光/黑暗主题之间切换。
当更改此值时,建议使用简单的JavaScript值dirtyCheck(root, true) ,而不是为此变量创建大量订阅,而是使用简单的JavaScript值和整个UI子树。
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 >
` ;只有一个没有一个没有任何静态属性的元素的模板将使用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 >
` ;
} ) ;可以通过与|分开来声明多个注释。操作员,例如/* 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 >
` ;将生成具有共享数据结构的两个不同模板:
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 ] ) ;通常,用于不同目的的Opcodes(Prop,儿童,状态)将具有相似的值,因此,当重复编辑Opcodes时,它们被视为可用于不同目的的整数的简单阵列。
将共享的strring(属性键,事件名称等)重复地将所有模板之间共享的一个数组( __IVI_STRINGS__ )。
IVI被设计为可嵌入的解决方案,因此可以将其集成到现有的框架或Web组件中。通过createRoot()函数实例化的基本根节点是使用Microtask队列安排更新。具有自定义调度算法的root节点可以通过使用defineRoot()函数定义新的root fortories创建。
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 > ;例如,要删除任何批处理并立即更新root subtree无效时,我们可以定义以下根节点:
import { defineRoot } from "ivi" ;
const createSyncRoot = defineRoot ( ( root ) => {
// Immediately triggers dirty checking.
dirtyCheck ( root ) ;
} ) ; requestAnimationFrame()来安排UI更新与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中,以过滤模块。 麻省理工学院