一個虛擬DOM庫,專注於簡單,模塊化,強大的功能和性能。
感謝Browserstack提供了對其出色的跨瀏覽器測試工具的訪問權限。
英語| 簡體中文|印地語
虛擬DOM很棒。它使我們能夠表達應用程序作為其狀態函數的看法。但是現有的解決方案太腫了,太慢,缺乏功能,對OOP有偏見,/或缺乏我需要的功能。
snabbdom由極其簡單,性能和可擴展的核心組成,僅為200 sloc。它提供了一個模塊化體系結構,具有豐富的功能,可通過自定義模塊進行擴展。為了保持核心簡單,所有非必需功能均委派給模塊。
您可以將Snabbdom塑造成任何想要的東西!選擇,選擇和自定義所需的功能。另外,您只需使用默認擴展名,並獲得具有高性能,小尺寸以及下面列出的所有功能的虛擬DOM庫。
h功能,用於輕鬆創建虛擬DOM節點。h助手一起工作。 import {
init ,
classModule ,
propsModule ,
styleModule ,
eventListenersModule ,
h
} from "snabbdom" ;
const patch = init ( [
// Init patch function with chosen modules
classModule , // makes it easy to toggle classes
propsModule , // for setting properties on DOM elements
styleModule , // handles styling on elements with support for animations
eventListenersModule // attaches event listeners
] ) ;
const container = document . getElementById ( "container" ) ;
const vnode = h (
"div#container.two.classes" ,
{ on : { click : ( ) => console . log ( "div clicked" ) } } ,
[
h ( "span" , { style : { fontWeight : "bold" } } , "This is bold" ) ,
" and this is just normal text" ,
h ( "a" , { props : { href : "/foo" } } , "I'll take you places!" )
]
) ;
// Patch into empty DOM element – this modifies the DOM as a side effect
patch ( container , vnode ) ;
const newVnode = h (
"div#container.two.classes" ,
{ on : { click : ( ) => console . log ( "updated div clicked" ) } } ,
[
h (
"span" ,
{ style : { fontWeight : "normal" , fontStyle : "italic" } } ,
"This is now italic type"
) ,
" and this is still just normal text" ,
h ( "a" , { props : { href : "/bar" } } , "I'll take you places!" )
]
) ;
// Second `patch` invocation
patch ( vnode , newVnode ) ; // Snabbdom efficiently updates the old view to the new state initpatchhfragment (實驗)toVNodeinit鉤子insert鉤remove鉤destroy鉤子remove上設置屬性destroySnabbdom的核心僅提供最重要的功能。它旨在盡可能簡單,同時仍然可以快速且可擴展。
init核心僅公開一個單個功能init 。該init獲取模塊的列表,並返回使用指定的模塊集的patch功能。
import { classModule , styleModule } from "snabbdom" ;
const patch = init ( [ classModule , styleModule ] ) ;patch init返回的patch功能需要兩個參數。第一個是代表當前視圖的DOM元素或vnode。第二個是代表新的更新視圖的vnode。
如果傳遞帶有父的DOM元素,則newVnode將變成DOM節點,並且傳遞的元素將被創建的DOM節點替換。如果傳遞了舊的vnode,則snabbdom將有效地修改它以匹配新Vnode中的描述。
任何傳遞的舊Vnode都必須是從上一個調用到patch中的結果VNODE。這是必要的,因為Snabbdom將信息存儲在VNode中。這使得實現了更簡單,更具性能的體系結構。這也避免了創建新的舊Vnode樹。
patch ( oldVnode , newVnode ) ; 雖然沒有專門用於從其安裝點元素中刪除vnode樹的API,但幾乎實現此目的的一種方法是將評論Vnode作為patch的第二個參數,例如:
patch (
oldVnode ,
h ( "!" , {
hooks : {
post : ( ) => {
/* patch complete */
}
}
} )
) ;當然,然後在安裝點仍然有一個評論節點。
h建議您使用h創建Vnodes。它接受標籤/選擇器作為字符串,可選的數據對象和一個可選的字符串或一個兒童數組。
import { h } from "snabbdom" ;
const vnode = h ( "div#container" , { style : { color : "#000" } } , [
h ( "h1.primary-title" , "Headline" ) ,
h ( "p" , "A paragraph" )
] ) ;fragment (實驗)注意:此功能目前是實驗性的,必須選擇。它的API可以在沒有主要版本的情況下更改。
const patch = init ( modules , undefined , {
experimental : {
fragments : true
}
} ) ;創建一個虛擬節點,該節點將轉換為包含給定兒童的文檔片段。
import { fragment , h } from "snabbdom" ;
const vnode = fragment ( [ "I am" , h ( "span" , [ " a" , " fragment" ] ) ] ) ;toVNode將DOM節點轉換為虛擬節點。特別適合修補已有的服務器端生成的HTML內容。
import {
init ,
styleModule ,
attributesModule ,
h ,
toVNode
} from "snabbdom" ;
const patch = init ( [
// Initialize a `patch` function with the modules used by `toVNode`
attributesModule // handles attributes from the DOM node
datasetModule , // handles `data-*` attributes from the DOM node
] ) ;
const newVNode = h ( "div" , { style : { color : "#000" } } , [
h ( "h1" , "Headline" ) ,
h ( "p" , "A paragraph" ) ,
h ( "img" , { attrs : { src : "sunrise.png" , alt : "morning sunrise" } } )
] ) ;
patch ( toVNode ( document . querySelector ( ".container" ) ) , newVNode ) ;鉤子是鉤住DOM節點生命週期的一種方法。 Snabbdom提供了豐富的鉤子。鉤子既由模塊使用來擴展snabbdom,又在正常代碼中用於虛擬節點生命中所需點執行任意代碼。
| 姓名 | 何時觸發 | 回調的論點 |
|---|---|---|
pre | 補丁過程開始 | 沒有任何 |
init | 已經添加了一個vnode | vnode |
create | 基於VNode創建了DOM元素 | emptyVnode, vnode |
insert | 一個元素已插入到DOM中 | vnode |
prepatch | 即將修補一個元素 | oldVnode, vnode |
update | 一個元素正在更新 | oldVnode, vnode |
postpatch | 一個元素已修補 | oldVnode, vnode |
destroy | 元素直接或間接地被刪除 | vnode |
remove | 一個元素直接從DOM刪除 | vnode, removeCallback |
post | 修補程序完成 | 沒有任何 |
以下掛鉤可用於模塊: pre , create , update , destroy , remove , post 。
各個元素的hook屬性中可用以下鉤子: init , create , insert , prepatch , update , postpatch , destroy , remove 。
要使用鉤子,請將它們作為對像傳遞到數據對象參數的hook場。
h ( "div.row" , {
key : movie . rank ,
hook : {
insert : ( vnode ) => {
movie . elmHeight = vnode . elm . offsetHeight ;
}
}
} ) ; init鉤子在找到新的虛擬節點時,在補丁過程中調用了此鉤。在Snabbdom以任何方式處理節點之前,該鉤子被調用。即,在基於VNode創建DOM節點之前。
insert鉤一旦將Vnode的DOM元素插入文檔中,並且完成了補丁週期的其餘部分,則將調用此掛鉤。這意味著您可以進行DOM測量(例如安全地在此鉤中使用getBoundingClientRect,因為知道之後不會更改元素會影響插入元素的位置。
remove鉤允許您掛接去除元素。一旦將vnode從DOM刪除後,掛鉤就被稱為。處理功能同時接收VNode和回調。您可以通過回調控制和延遲刪除。掛鉤完成業務後,應調用回調,只有在所有remove鉤子都調用其回調後,該元素才會被刪除。
僅當要從父母那裡刪除元素時,才會觸發鉤子 - 當它是被刪除的元素的孩子時。為此,請參閱destroy鉤子。
destroy鉤子當將其DOM元素從DOM中刪除或從DOM中刪除其父時,該掛鉤將在虛擬節點上調用。
要查看此鉤子和remove鉤之間的區別,請考慮一個示例。
const vnode1 = h ( "div" , [ h ( "div" , [ h ( "span" , "Hello" ) ] ) ] ) ;
const vnode2 = h ( "div" , [ ] ) ;
patch ( container , vnode1 ) ;
patch ( vnode1 , vnode2 ) ;此處的destroy是針對內部div元素及其包含的span元素觸發的。另一方面, remove僅在div元素上觸發,因為它是唯一與父母分離的元素。
例如,您可以在刪除元素時使用remove來觸發動畫,並使用destroy鉤子額外動畫刪除元素的孩子的消失。
模塊通過註冊全局聽眾的鉤子來起作用。模塊只是詞典映射掛鉤名稱到函數。
const myModule = {
create : ( oldVnode , vnode ) => {
// invoked whenever a new virtual node is created
} ,
update : ( oldVnode , vnode ) => {
// invoked whenever a virtual node is updated
}
} ;通過這種機制,您可以輕鬆地增強snabbdom的行為。對於演示,請查看默認模塊的實現。
這描述了核心模塊。所有模塊都是可選的。 JSX示例假設您正在使用此庫提供的jsx Pragma。
類模塊提供了一種簡單的方法,可以動態切換元素上的類。它期望class數據屬性中的對象。該對象應將類名映射到布爾值,以指示該類是否應保持或進行VNODE。
h ( "a" , { class : { active : true , selected : false } } , "Toggle" ) ;在JSX中,您可以使用這樣的class :
< div class = { { foo : true , bar : true } } />
// Renders as: <div class="foo bar"></div>允許您在DOM元素上設置屬性。
h ( "a" , { props : { href : "/foo" } } , "Go to Foo" ) ;在JSX中,您可以使用這樣的props :
< input props = { { name : "foo" } } />
// Renders as: <input name="foo" /> with input.name === "foo"只能設置屬性。沒有刪除。即使瀏覽器允許添加和刪除自定義屬性,此模塊也不會嘗試刪除。這是有道理的,因為本地域屬性無法刪除。而且,如果您使用自定義屬性來存儲值或在DOM上引用對象,則請考慮使用數據 - *屬性。也許是通過數據集模塊。
與prop相同,但在dom元素上設置屬性而不是屬性。
h ( "a" , { attrs : { href : "/foo" } } , "Go to Foo" ) ;在JSX中,您可以使用類似的attrs :
< div attrs = { { "aria-label" : "I'm a div" } } />
// Renders as: <div aria-label="I'm a div"></div>使用setAttribute添加和更新屬性。如果以前已添加/集合併且不再存在於attrs對像中的屬性,則使用removeAttribute將其從DOM元素的屬性列表中刪除。
對於布爾屬性(例如disabled , hidden , selected ...),含義不取決於屬性值( true或false ),而是取決於DOM元素中屬性本身的存在/不存在。這些屬性通過模塊的處理方式有所不同:如果將布爾屬性設置為虛假值( 0 , -0 , null , false ,nan, NaN ,nan, undefined或empty string( "" )),則該屬性將從DOM元素的屬性列表中刪除。
允許您在DOM元素上設置自定義數據屬性( data-* )。然後可以使用htmlelement.dataset屬性訪問這些。
h ( "button" , { dataset : { action : "reset" } } , "Reset" ) ;在JSX中,您可以使用這樣的dataset :
< div dataset = { { foo : "bar" } } />
// Renders as: <div data-foo="bar"></div>該樣式模塊是為了使您的HTML看起來光滑,動畫順利。以此為核心,您可以在元素上設置CSS屬性。
h (
"span" ,
{
style : {
border : "1px solid #bada55" ,
color : "#c0ffee" ,
fontWeight : "bold"
}
} ,
"Say my name, and every colour illuminates"
) ;在JSX中,您可以使用這樣的style :
< div
style = { {
border : "1px solid #bada55" ,
color : "#c0ffee" ,
fontWeight : "bold"
} }
/>
// Renders as: <div style="border: 1px solid #bada55; color: #c0ffee; font-weight: bold"></div> 支持CSS自定義屬性(又稱CSS變量),必須將其前綴--
h (
"div" ,
{
style : { "--warnColor" : "yellow" }
} ,
"Warning"
) ; 您可以將屬性指定為延遲。每當這些屬性更改時,直到下一幀之後才能應用更改。
h (
"span" ,
{
style : {
opacity : "0" ,
transition : "opacity 1s" ,
delayed : { opacity : "1" }
}
} ,
"Imma fade right in!"
) ;這使聲明性地將元素的輸入動畫起來變得容易。
不支持transition-property的all價值。
remove上設置屬性一旦將元素從DOM刪除,將在remove屬性中設置的樣式將生效。應用樣式應使用CSS過渡來動畫。只有完成所有樣式的動畫,才能從DOM中刪除該元素。
h (
"span" ,
{
style : {
opacity : "1" ,
transition : "opacity 1s" ,
remove : { opacity : "0" }
}
} ,
"It's better to fade out than to burn away"
) ;這使聲明性地將元素的刪除動畫起來變得容易。
不支持transition-property的all價值。
destroy h (
"span" ,
{
style : {
opacity : "1" ,
transition : "opacity 1s" ,
destroy : { opacity : "0" }
}
} ,
"It's better to fade out than to burn away"
) ;不支持transition-property的all價值。
事件聽眾模塊為附加事件聽眾提供了強大的功能。
您可以通過向對象的屬性提供與您要收聽的事件名稱相對on的屬性,將函數附加到VNode上的事件。當事件發生時,將調用該函數,並將傳遞給屬於該事件的對象。
function clickHandler ( ev ) {
console . log ( "got clicked" ) ;
}
h ( "div" , { on : { click : clickHandler } } ) ;在JSX中,您可以on使用:
< div on = { { click : clickHandler } } />Snabbdom允許在渲染器之間交換事件處理程序。這發生在沒有實際觸摸DOM上的事件處理程序的情況下發生。
但是,請注意,在VNodes之間共享事件處理程序時,應該要小心,因為該模塊用來避免重新結合事件處理程序的技術。 (而且通常,由於允許模塊突變給定的數據,因此不能保證在VNODE之間共享數據)。
特別是,您不應該做這樣的事情:
// Does not work
const sharedHandler = {
change : ( e ) => {
console . log ( "you chose: " + e . target . value ) ;
}
} ;
h ( "div" , [
h ( "input" , {
props : { type : "radio" , name : "test" , value : "0" } ,
on : sharedHandler
} ) ,
h ( "input" , {
props : { type : "radio" , name : "test" , value : "1" } ,
on : sharedHandler
} ) ,
h ( "input" , {
props : { type : "radio" , name : "test" , value : "2" } ,
on : sharedHandler
} )
] ) ;對於許多這樣的情況,您可以使用基於數組的處理程序(上述)。另外,只需確保每個節點on值上都唯一傳遞:
// Works
const sharedHandler = ( e ) => {
console . log ( "you chose: " + e . target . value ) ;
} ;
h ( "div" , [
h ( "input" , {
props : { type : "radio" , name : "test" , value : "0" } ,
on : { change : sharedHandler }
} ) ,
h ( "input" , {
props : { type : "radio" , name : "test" , value : "1" } ,
on : { change : sharedHandler }
} ) ,
h ( "input" , {
props : { type : "radio" , name : "test" , value : "2" } ,
on : { change : sharedHandler }
} )
] ) ; 當使用h函數創建虛擬節點時,SVG只是工作。 SVG元素是使用適當的名稱空間自動創建的。
const vnode = h ( "div" , [
h ( "svg" , { attrs : { width : 100 , height : 100 } } , [
h ( "circle" , {
attrs : {
cx : 50 ,
cy : 50 ,
r : 40 ,
stroke : "green" ,
"stroke-width" : 4 ,
fill : "yellow"
}
} )
] )
] ) ;另請參見SVG示例和SVG輪播示例。
某些瀏覽器(例如IE <= 11)不支持SVG元素中的classList屬性。由於類模塊在內部使用classList ,因此在這種情況下,除非您使用classList polyfill,否則它將無法使用。 (如果您不想使用polyfill,則可以將class屬性與屬性模塊一起使用)。
thunk函數採用一個選擇器,一個識別thunk的鍵,返回vnode的函數和可變的狀態參數。如果調用,渲染函數將接收狀態參數。
thunk(selector, key, renderFn, [stateArguments])
僅當更改renderFn或[stateArguments]數組長度或其元素更改時,才調用renderFn 。
key是可選的。當selector在Thunks兄弟姐妹之間不是唯一的時,應該提供它。這樣可以確保在擴散時始終正確匹配thunk。
Thunks是一種優化策略,可以在處理不變數據時使用。
考慮一個簡單的功能,用於創建基於數字的虛擬節點。
function numberView ( n ) {
return h ( "div" , "Number is: " + n ) ;
}該視圖僅取決於n 。這意味著,如果n不變,則創建虛擬DOM節點並將其與舊VNode進行修補是浪費的。為了避免開銷,我們可以使用thunk Helper功能。
function render ( state ) {
return thunk ( "num" , numberView , [ state . number ] ) ;
}而不是實際調用numberView函數,而是將虛擬vnode放在虛擬樹中。當snabbdom對此虛擬vnode貼在先前的vnode上時,它將比較n的值。如果n不變,它將僅重複使用舊的VNODE。這避免了重新創建數字視圖和差異過程。
此處的視圖功能只是一個示例。實際上,只有當您呈現出複雜的視圖時,thunks才有意義。
請注意,JSX片段仍然是實驗性的,必須選擇。有關詳細信息,請參見fragment部分。
將以下選項添加到您的tsconfig.json :
{
"compilerOptions" : {
"jsx" : " react " ,
"jsxFactory" : " jsx " ,
"jsxFragmentFactory" : " Fragment "
}
}然後確保您使用.tsx文件擴展名並導入文件頂部的jsx函數和Fragment函數:
import { Fragment , jsx , VNode } from "snabbdom" ;
const node : VNode = (
< div >
< span > I was created with JSX </ span >
</ div >
) ;
const fragment : VNode = (
< >
< span > JSX fragments </ span >
are experimentally supported
</ >
) ;將以下選項添加到您的Babel配置中:
{
"plugins" : [
[
" @babel/plugin-transform-react-jsx " ,
{
"pragma" : " jsx " ,
"pragmaFrag" : " Fragment "
}
]
]
}然後在文件頂部導入jsx函數和Fragment函數:
import { Fragment , jsx } from "snabbdom" ;
const node = (
< div >
< span > I was created with JSX </ span >
</ div >
) ;
const fragment = (
< >
< span > JSX fragments </ span >
are experimentally supported
</ >
) ; 特性
sel屬性指定Vnode的HTML元素,可選地將其id由A #和零或更多類。每個類別由A前綴. 。該語法靈感來自CSS選擇器。這裡有幾個例子:
div#container.bar.baz - 帶有ID container以及類bar和baz div元素。li - 一個沒有id和類的li元素。button.alert.primary - 帶有兩個類alert的button元素primary選擇器是靜態的,也就是說,它不應在元素的生命週期內改變。要設置動態id請使用道具模塊並設置動態類,請使用類模塊。
由於選擇器是靜態的,因此snabbdom將其用作Vnodes身份的一部分。例如,如果兩個孩子vnodes
[ h ( "div#container.padding" , children1 ) , h ( "div.padding" , children2 ) ] ;被修補
[ h ( "div#container.padding" , children2 ) , h ( "div.padding" , children1 ) ] ;然後,snabbdom使用選擇器來識別vnodes並將其重新排序,而不是創建新的DOM元素。選擇器的這種使用避免了在許多情況下需要指定密鑰的需要。
虛擬節點的.data屬性是添加模塊的信息以訪問和操縱該模塊時的位置。添加樣式,CSS類,屬性等。
數據對像是h()的(可選)第二個參數
例如, h('div', {props: {className: 'container'}}, [...])將帶有一個虛擬節點
( {
props : {
className : "container"
}
} ) ;作為.data對象。
虛擬節點的.children屬性是創建過程中h()的第三個(可選)參數。 .children只是一系列虛擬節點,應該在創建時作為父dom節點的孩子添加。
例如h('div', {}, [ h('h1', {}, 'Hello, World') ])將使用一個虛擬節點
[
{
sel : "h1" ,
data : { } ,
children : undefined ,
text : "Hello, World" ,
elm : Element ,
key : undefined
}
] ;作為.children財產。
當僅使用一個具有document.createTextNode()的單個孩子而創建虛擬節點時,創建.text屬性。
例如: h('h1', {}, 'Hello')將使用Hello作為.text屬性創建虛擬節點。
虛擬節點的.elm屬性是指向Snabbdom創建的真實DOM節點的指針。此屬性對於在鉤子和模塊中進行計算非常有用。
當您的.data對象內提供鍵時,創建.key屬性。 .key這對於列表重新排序之類的東西非常有用。鍵必須是字符串或數字以進行正確的查找,因為它在內部將其作為鍵/值對存儲在對象內部,其中.key是鍵,而值是創建的.elm屬性。
如果提供,則.key屬性必須在同胞元素之間是唯一的。
例如: h('div', {key: 1}, [])將創建一個帶有1個值1的.key屬性的虛擬節點對象。
Snabbdom是一個低級虛擬DOM庫。關於如何構建應用程序,它不予以審查。
以下是使用snabbdom構建應用程序的一些方法。
如果您使用snabbdom以另一種方式構建應用程序,請務必共享。
與snabbdom有關的軟件包應用snabbdom關鍵字標記並在NPM上發布。可以使用查詢字符串keywords:snabbdom找到它們。
Uncaught NotFoundError: Failed to execute 'insertBefore' on 'Node':
The node before which the new node is to be inserted is not a child of this node.
此錯誤的原因是補丁之間的VNODE(請參見代碼示例),Snabbdom存儲在虛擬DOM節點內的實際DOM節點隨著性能的改進而傳遞給它,因此不支持補丁之間的節點。
const sharedNode = h ( "div" , { } , "Selected" ) ;
const vnode1 = h ( "div" , [
h ( "div" , { } , [ "One" ] ) ,
h ( "div" , { } , [ "Two" ] ) ,
h ( "div" , { } , [ sharedNode ] )
] ) ;
const vnode2 = h ( "div" , [
h ( "div" , { } , [ "One" ] ) ,
h ( "div" , { } , [ sharedNode ] ) ,
h ( "div" , { } , [ "Three" ] )
] ) ;
patch ( container , vnode1 ) ;
patch ( vnode1 , vnode2 ) ;您可以通過創建對象的淺副本來解決此問題(在此使用對像差異語法):
const vnode2 = h ( "div" , [
h ( "div" , { } , [ "One" ] ) ,
h ( "div" , { } , [ { ... sharedNode } ] ) ,
h ( "div" , { } , [ "Three" ] )
] ) ;另一個解決方案是將共享的vnodes包裹在出廠功能中:
const sharedNode = ( ) => h ( "div" , { } , "Selected" ) ;
const vnode1 = h ( "div" , [
h ( "div" , { } , [ "One" ] ) ,
h ( "div" , { } , [ "Two" ] ) ,
h ( "div" , { } , [ sharedNode ( ) ] )
] ) ; 在提供了幾天的機會之後,應合併社區可能會關心提供反饋的拉動請求。