IVI - это легкая встроенная декларативная библиотека пользовательского интерфейса.
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,7 КБ (минимизированный+brotli). Он включает в себя полное время выполнения для декларативного рендеринга пользовательского интерфейса. Предварительные шаблоны оптимизированы для размера кода и холодного отпуска.
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.
// rollup.config.mjs
import { ivi } from "@ivi/rollup-plugin" ;
export default {
input : "src/main.js" ,
output : {
file : "bundle.js" ,
} ,
plugins : [ ivi ( ) ]
} ; Язык шаблона IVI имеет HTML-подобный синтаксис с дополнительным синтаксисом для свойств DOM, событий и удаления пробелов.
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 , diffs против значения DOM.<div ~name="value" /> - статический стиль <div style="name:value;"> .<div ~name=${expr} /> - Dynamic Style element.style.setProperty(name, expr) .<div @name=${expr} /> - element 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 значение, он будет удален из элемента DOM с помощью метода Element.removeAttribute(..) .
<div .name=${expr} /> - element[name] = expr .<div *name=${expr} /> - element[name] = expr , diffs против значения DOM. Свойства присваиваются с помощью элемента оператора назначения. Element.name = value .
Разница со значением DOM полезно при использовании, когда мы используем значения <input> , чтобы избежать запуска ненужных событий input .
<div ~name="value" /> - статический стиль <div style="value"> .<div ~name=${expr} /> - Dynamic Style element.style.setProperty(name, expr) . Статические стили автоматически объединяются с атрибутом :style="value" .
Динамические стили назначены методом CSSStyleDeclaration.setProperty(..) .
Когда стиль имеет undefined , null или false значение, он будет удален с помощью метода CSSStyleDeclaration.removeProperty(..) .
<div @name=${expr} /> - element 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 ;Функция директивы вызывается только тогда, когда шаблон создается с другой функцией, поэтому, если мы собираемся повторно использовать одну и ту же функцию, его можно использовать в качестве элемента 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 или переключатели для генерации динамического содержания на основе условий выполнения.
Это означает, что вы можете создавать шаблоны со сложной логикой, которая условно производит различный контент в зависимости от того, что происходит в вашем приложении. Вы можете гнездовать выражения шаблонов друг внутри друг друга, чтобы создать более сложные шаблоны, и вы можете сохранить результаты шаблонов в переменных, чтобы использовать их позже в вашем коде.
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> как набор элементов верхнего уровня, как и с одним корневым элементом.
Эта функция обеспечивает большую гибкость при создании комплексных компонентов пользовательского интерфейса, поскольку она позволяет создавать компоненты, которые генерируют динамическое количество элементов верхнего уровня в зависимости от их входа.
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, прикрепленных к документу, перемещается, он может создавать уведомление MutationObserver. И многие популярные расширения используют мутационные наблюдатели для наблюдения за всем поддереем документов, поэтому каждая операция insertBefore может стать довольно дорогостоящей, когда она используется вне песочных ящиков.
Компоненты могут быть либо государственными, либо без сохранения состояния. Компоненты состояния используются, когда вам нужно управлять состоянием, которое меняется с течением времени, например, пользовательский ввод, сетевые запросы или анимация.
Компоненты Stateful объявляются функцией 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" } ) ,
) ;Компоненты Stateful используют закрытие 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() .
Существуют высокоуровневые API, такие как useState() или useReducer() , которые используют функцию недействительного () низкого уровня 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 создает корневой узел, который использует очередь микротаски для планирования обновлений.
function createRoot (
parentElement : Element ,
nextNode : Node | null = null ,
) : Root ;parentElement - родительский элемент DOM.nextNode - Next 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 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 - необязательная функция, которая проверяет входные свойства для изменений и используется в качестве подсказки для оптимизации, чтобы уменьшить ненужные обновления, когда свойства не изменяются. Когда корневой поддерек обновляется с помощью опции forceUpdate , 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 - Uncont 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 Mation и сеттера состояния.
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 Getter и действие.
Побочные эффекты позволяют указать, как ваши компоненты должны вести себя с внешними системами, такими как императивные вызовы 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 создает диспетчер -диспетчер, который находит ближайший узел Dom Dom и издает CustomEvent метод 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 - тип события.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 - это функция NOOP, которая всегда возвращает true значение.
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 .Когда планировщик решает обновить корневой узел с грязным поддереем, он запускает алгоритм грязного проверки. Этот алгоритм идет сверху вниз в порядке, посещая все узлы с грязным флагом поддереев, пока не достигнет грязного компонента и не обновляет его.
DirtySubtree начинает проверять своих детей.Dirty флагом, запускает обновление.Dirty флагом, запускает обновление. Одна из причин, по которой основная библиотека такая мала, заключается в том, что алгоритм обновления реализован в порядке RTL. Алгоритм, который выполняет обновления в RTL -заказе, упрощает множество сложных проблем с обновлениями DOM. Основная проблема с обновлениями DOM заключается в том, что когда мы начнем обновлять структуру дерева DOM, нам необходимо иметь ссылку на родительского и следующего узла DOM, чтобы мы могли использовать parent.insertBefore(newNode, nextNode) . В большинстве случаев легко получить следующий узел DOM, но есть кромки, например, когда у нас есть два смежных условных выражения, и одно из их состояний заключается в том, что он полностью удаляет узел DOM из дерева, или два соседних компонента с условными условиями на их корнях и т. Д.
Большинство библиотек занимаются этими краями, введя маркерные узлы DOM (комментарий или пустой текстовый узел). Например, чтобы реализовать условные выражения, мы можем добавить пустой текстовый узел, когда условное не визует какого -либо узла DOM, и когда условное входит в состояние, когда ему необходимо добавить узел DOM, он будет использовать маркерный узел в качестве следующего эталона узла DOM. Алгоритм обновления RTL в IVI не использует маркерные узлы.
Алгоритм RTL, который используется в IVI, также значительно облегчает реализацию смещений узлов без введения каких -либо дополнительных путей кода, фрагментов и практически всего, что включает в себя обновление структуры DOM.
Каждый вызов, который создает шаблон, имеет уникальную идентичность, поэтому даже идентичные шаблоны, созданные из разных сайтов вызовов, не смогут отличаться друг от друга.
function TemplateUniqueIdentity ( condition , text ) {
return ( condition )
? html `div ${ text } `
: html `div ${ text } ` ;
} В приведенном выше примере, когда condition изменяется, вместо обновления текстового узла алгоритм обновления заменит весь элемент DIV новым.
Существуют некоторые варианты использования, которые требуют много частых чтений из реактивной переменной. И всякий раз, когда эта переменная меняется, она влияет на множество узлов пользовательского интерфейса, например, переключение между световыми/темными темами.
Вместо того, чтобы создавать множество подписок на эти переменные, рекомендуется использовать простые значения JavaScript и повторный подборе пользовательского интерфейса dirtyCheck(root, true) , когда эти значения изменяются.
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 ( ) ) ; Клонирование шаблона - это оптимизация, которая используется для клонирования шаблонов HTML с помощью метода Node.cloneNode() .
По умолчанию клонирование шаблона включено для всех шаблонов. Но иногда было бы расточительно создать шаблон для клонирования и экземпляра из него, когда этот шаблон отображается только один раз.
Чтобы отключить клонирование, шаблон должен иметь ведущий комментарий /* 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.
Дерево пользовательского интерфейса реализовано с помощью состоятельного 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 есть Saturful Node 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 ] ) ;Довольно часто, опкоды, которые используются для разных целей (реквизит, ребенок, состояние) будут иметь аналогичные значения, поэтому, когда опкомтизируются дедуплистские, они рассматриваются как простые массивы с целыми числами, которые могут использоваться для разных целей.
Общие стрины (ключи от атрибутов, имена событий и т. Д.) Образуются в один массив ( __IVI_STRINGS__ ), который разделяется между всеми шаблонами.
IVI разработан как встроенное решение, так что его можно интегрировать в существующие рамки или веб -компоненты. Основной корневой узел, созданный с помощью функции createRoot() использует очередь микротаски для планирования обновлений. Корневые узлы с пользовательским алгоритмом планирования могут быть созданы путем определения новых корневых фабрик с помощью функции 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 запускается, кнопка отправки переходит в состояние отключения. Самый простой способ избежать подобных проблем - использовать MicroTasks для партии. Но если вы действительно хотите добавить планирование rAF , можно решить подобные проблемы, введя некоторые примитивы синхронизации:
import { uiReady } from "my-custom-scheduler" ;
const onSubmit = async ( ev ) => {
await uiReady ( ) ;
submit ( ) ;
} ; Среда выполнения IVI не зависит от каких -либо внешних библиотек.
IVI Dev Tools имеет минимальный набор зависимостей:
@rollup/pluginutils используется в плагине Rollup @ivi/rollup-plugin для фильтрации модулей. Грань