一个虚拟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 ( ) ] )
] ) ; 在提供了几天的机会之后,应合并社区可能会关心提供反馈的拉动请求。