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 。<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 。屬性分配了分配運算符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中,以過濾模塊。 麻省理工學院