Description
Snabbdom
注重简单性、模块化、强大特性和性能的虚拟DOM库。
Table of contents
- 介绍
- 功能
- 内联例子
- 例子
- 核心文档
- 模块文档
- 帮助
- 虚拟节点文档
- 结构化应用
- 常见错误
为什么
虚拟DOM是非常棒的。它允许我们将应用程序的视图表示为它的状态的函数。但是现有的解决方案太过臃肿、太慢、缺少功能、API偏向于OOP(面向对象的程序设计)和缺少我需要的特性。
介绍
Snabbdom包含一个非常简单、高性能、可扩展的核心,只有≈200行。为了可以通过自定义模块进行扩展它提供了具有功能丰富的模块化体系结构。为了保持核心的简单,所有非必要的功能都委托给了模块。
你可以把Snabbdom塑造成你想要的样子!选择并定制您想要的功能。或者,您也可以只使用默认的扩展就可以获得一个具有高性能、小尺寸和下面列出的所有功能的虚拟DOM库。
功能
- 核心功能
- 大约200 SLOC—您可以轻松地阅读整个内核并完全理解它的工作原理。
- 可以通过模块扩展。
- 每个vnode和模块都有一组丰富的钩子,可以挂钩到diff和patch过程的任何部分。
- 极好的性能。Snabbdom是虚拟DOM基准测试中速度最快的虚拟DOM库之一。
- Patch函数具有函数签名,等同于reduce/scan函数。允许更容易地与FRP库集成。
- 模块功能
h
函数可以很方便创建虚拟DOM节点。h
函数也可以操作 SVG。- 操作复杂的 CSS 动画的功能。
- 强大的事件监听器功能。
thunk
函数可以进一步优化 diff 和 patch 过程。
- 第三方功能
snabbdom-pragma
使snabbdom支持JSX。snabbdom-to-html
使服务端(NodeJS)支持输出HTML。snabbdom-helpers
可以创建简洁的虚拟DOM。snabby
支持模板字符串。snabbdom-looks-like
支持虚拟DOM断言。
内联例子
var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var container = document.getElementById('container');
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
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);
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
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
例子
核心文档
Snabbdom的核心只提供最基本的功能。它被设计得尽可能简单,同时仍然是快速的和可扩展的。
snabbdom.init
核心仅暴露出一个函数snabbdom.init
。init
接收一个模块列表,并返回一个使用指定模块集的patch
函数。
var patch = snabbdom.init([
require('snabbdom/modules/class').default,
require('snabbdom/modules/style').default,
]);
patch
init
返回的patch
函数有两个参数。第一个是表示当前视图的DOM元素或vnode。第二个是表示更新后的新视图的vnode。
如果传递带有父节点的DOM元素,newVnode
将被转换为DOM节点,传递的元素将被创建的DOM节点替换。如果传递旧的vnode, Snabbdom将有效地修改它以匹配新vnode中的描述。
传递的任何旧vnode都必须是上一个patch
调用的结果vnode。这是必要的,因为Snabbdom将信息存储在vnode中。这使得实现更简单、更高性能的体系结构成为可能。这也避免了创建新的旧vnode树。
patch(oldVnode, newVnode);
snabbdom/h
建议使用snabbdom/h
创建虚拟节点(vnodes)。h
函数接收一个字符串形式的标签/选择器、一个可选的数据对象、一个可选的字符串或数组作为子代。
var h = require('snabbdom/h').default;
var vnode = h('div', {style: {color: '#000'}}, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
]);
snabbdom/tovnode
将DOM节点转换为虚拟节点。特别适合修补已存在的服务器端生成内容。
var snabbdom = require('snabbdom')
var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var toVNode = require('snabbdom/tovnode').default;
var newVNode = h('div', {style: {color: '#000'}}, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
]);
patch(toVNode(document.querySelector('.container')), newVNode)
Hooks
钩子是一种挂钩到DOM节点生命周期的方法。Snabbdom提供了丰富的钩子可以选择。模块使用钩子来扩展Snabbdom,在普通代码中,钩子用于在虚拟节点生命周期的期望点执行任意代码。
概览
Name | Triggered when | Arguments to callback |
---|---|---|
pre |
patch过程开始 | none |
init |
一个虚拟节点被添加 | vnode |
create |
基于一个虚拟节点,一个DOM被创建 | emptyVnode, vnode |
insert |
一个元素被插入到DOM中 | vnode |
prepatch |
一个元素即将被修补(patched) | oldVnode, vnode |
update |
一个元素正在被更新 | oldVnode, vnode |
postpatch |
一个元素已经被修补完成(patched) | oldVnode, vnode |
destroy |
元素被直接或间接删除 | vnode |
remove |
元素将直接从DOM中删除 | vnode, removeCallback |
post |
修补(patch)过程结束 | none |
以下钩子可用于模块:pre、create、update、destroy、remove、post。
以下钩子可用于单个元素的钩子属性:init、create、insert、prepatch、update、postpatch、destroy、remove。
使用
要使用钩子,将它们作为对象传递给数据对象参数的hook
字段。
h('div.row', {
key: movie.rank,
hook: {
insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight; }
}
});
init
钩子
当patch过程中发现新的虚拟节点时,这个hook会被调用。这个hook在Snabbdom以任何方式处理节点之前被调用。即,在它创建基于vnode的DOM节点之前。
insert
钩子
一旦将vnode生成的DOM元素插入document并完成patch周期的其余部分,这个hook就会被调用。这意味着你可以做DOM测量(比如在这个钩子里使用getBoundingClientRect
是安全的,因为没有元素会被改变导致插入的元素的位置受到影响。)
remove
钩子
允许你挂钩到移除的元素。一旦一个vnode从DOM中移除,这个hook将会被调用。这个处理函数接收这个vnode和一个回调。你可以使用回调控制和延迟移除。这个回调应该在hook完成它的业务之后被调用,并且只有在所有的remove
钩子都调用了它们的回调之后,元素才会被删除。
当一个元素被它的父元素移除,这个hook才会被调用——除非它是已经被移除的元素的子元素。为此,请参考destroy
钩子。
destroy
钩子
当虚拟节点的DOM元素从DOM中删除或它的父节点从DOM中删除时,将在该节点上调用这个hook。
要查看此钩子和remove钩子之间的区别,请考虑一个示例。
var vnode1 = h('div', [h('div', [h('span', 'Hello')])]);
var vnode2 = h('div', []);
patch(container, vnode1);
patch(vnode1, vnode2);
在这里,对于内部div
元素及其包含的span
元素,都会触发destroy
钩子。然而,remove
钩子仅在内部div
元素上触发,因为它是惟一与其父元素分离的元素。
例如,当一个元素被删除时,您可以使用remove
钩子来触发一个动画,并使用destroy
钩子来增加被删除元素的子元素消失的动画效果。
创建模块
模块通过为钩子注册全局监听器来工作。模块是一个简单的字典,将钩子名映射到函数。
var myModule = {
create: function(oldVnode, vnode) {
// invoked whenever a new virtual node is created
},
update: function(oldVnode, vnode) {
// invoked whenever a virtual node is updated
}
};
有了这个机制,您可以轻易地增强Snabbdom的功能。要进行演示,请查看默认模块的实现。
模块文档
这描述了核心模块。所有的模块都是可选的。
The class module,类模块
类模块提供了一种动态切换元素上的类的简单方法。它期待class
的data属性中有一个对象。对象应该将类名映射到布尔值,布尔值指示类应该留在vnode上还是被移除。
h('a', {class: {active: true, selected: false}}, 'Toggle');
The props module,属性模块
允许设置DOM元素的属性。
h('a', {props: {href: '/foo'}}, 'Go to Foo');
The attributes module,特性模块
和属性模块一样,但是是设置特性而不是属性。
h('a', {attrs: {href: '/foo'}}, 'Go to Foo');
使用setAttribute
添加和更新特性。如果先前添加或设置的特性不再出现在attrs
对象中,则使用removeAttribute
从DOM元素的特性列表中删除该特性。
对于布尔属性(例如disabled
, hidden
, selected
…),它们的作用并不取决于特性值(true或false),而是取决于特性本身在DOM元素中的存在与否。模块以不同的方式处理这些特性:如果布尔属性设置为falsy值(0、-0、null、false、NaN、undefined
或空字符串(""
)),则该特性将从DOM元素的特性列表中删除。
The dataset module,数据集模块
允许在DOM元素上设置自定义数据属性(data-*
)。然后可以使用HTMLElement访问这些元素。数据集属性。
h('button', {dataset: {action: 'reset'}}, 'Reset');
The style module,样式模块
样式模块用于使您的HTML看起来流畅和动画流畅。它的核心功能是允许在元素上设置CSS属性。
h('span', {
style: {border: '1px solid #bada55', color: '#c0ffee', fontWeight: 'bold'}
}, 'Say my name, and every colour illuminates');
请注意,如果样式属性作为样式对象的属性被移除,样式模块并不会移除它们。为了移除一个样式,应该将其设置为空字符串。
h('div', {
style: {position: shouldFollow ? 'fixed' : ''}
}, 'I, I follow, I follow you');
Custom properties (CSS variables)
CSS自定义属性(又名CSS变量),它们必须以--
为前缀。
h('div', {
style: {'--warnColor': 'yellow'}
}, 'Warning');
Delayed properties
可以将属性指定为延迟作用。当这些属性发生变化时,直到下一帧之后才会生效。
h('span', {
style: {opacity: '0', transition: 'opacity 1s', delayed: {opacity: '1'}}
}, 'Imma fade right in!');
这使得声明式地驱动元素的入场动画变得很容易。
Set properties on remove
在remove
属性中设置的样式将在元素即将从DOM中移除时生效。应用的样式应该用CSS过渡动画化。只有在所有样式完成动画之后,元素才会从DOM中移除。
h('span', {
style: {opacity: '1', transition: 'opacity 1s',
remove: {opacity: '0'}}
}, 'It\'s better to fade out than to burn away');
这使得声明式地驱动元素的出场动画变得很容易。
Set properties on destroy
h('span', {
style: {opacity: '1', transition: 'opacity 1s',
destroy: {opacity: '0'}}
}, 'It\'s better to fade out than to burn away');
Eventlisteners module,事件监听器模块
事件监听器模块提供了附加事件监听器的强大功能。
可以将函数附加到vnode上的事件,方法是通过在on
属性上提供一个对象,该对象的属性对应你想要监听的事件。当事件发生时,函数将会被调用,并传递属于该函数的事件对象。
function clickHandler(ev) { console.log('got clicked'); }
h('div', {on: {click: clickHandler}});
然而,通常情况下,你对事件对象本身并不是真正感兴趣。通常,你会有一些与这个触发事件的元素的相关联的数据,并且你想要传递的就是这些数据。
假设一个计数器应用程序,它有三个按钮,一个将计数器增加1,一个将计数器增加2,另一个将计数器增加3。您并不真正关心按下了哪个按钮。相反,您感兴趣的是与单击按钮相关联的数字。事件监听器模块允许通过在指定的事件属性上提供一个数组来表示。数组第一个元素应该是一个将会被调用的函数,当事件发生时第二个元素将会被传入这个函数。
function clickHandler(number) { console.log('button ' + number + ' was clicked!'); }
h('div', [
h('a', {on: {click: [clickHandler, 1]}}),
h('a', {on: {click: [clickHandler, 2]}}),
h('a', {on: {click: [clickHandler, 3]}}),
]);
不仅给定的参数会被传递到每个处理器,当前事件和vnode也会被添加到参数列表。它还支持通过指定一个处理器数组来为每个事件使用多个侦听器:
stopPropagation = function(ev) { ev.stopPropagation() }
sendValue = function(func, ev, vnode) { func(vnode.elm.value) }
h('a', { on:{ click:[[sendValue, console.log], stopPropagation] } });
Snabbdom允许在渲染函数之间交换事件处理器。这并不需要确实接触到附加到DOM的事件处理器。
但是,请注意,在vnode之间共享事件处理器时应该小心,因为这个模块使用了避免将事件处理器重新绑定到DOM的技术。(通常,不能保证在vnodes之间共享数据,因为允许模块修改提供的数据)。
特别地,你不应该像这样做:
// Does not work
var sharedHandler = {
change: function(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
var sharedHandler = function(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}})
]);
帮助
SVG
SVG只在使用h
函数创建虚拟节点时有效。使用适当的名称空间自动创建SVG元素。
var 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'}})
])
]);
Using Classes in SVG Elements
某些浏览器(如IE <=11)不支持SVG元素中的classList属性。因此,类模块(在内部使用classList属性)将不适用于这些浏览器。
SVG元素的类选择器在版本0.6.7中工作得很好。
可以使用特性模块和如下所示的数组,为这些情况向SVG元素添加动态类:
h('svg', [
h('text.underline', { // 'underline' is a selector class, remain unchanged between renders.
attrs: {
// 'active' and 'red' are dynamic classes, they can change between renders
// so we need to put them in the class attribute.
// (Normally we'd use the classModule, but it doesn't work inside SVG)
class: [isActive && "active", isColored && "red"].filter(Boolean).join(" ")
}
},
'Hello World'
)
])
Thunks
thunk
函数接受一个选择器、一个标识thunk的键、一个返回vnode的函数和一个可变数量的状态参数。如果被调用,render函数将接收状态参数。
thunk(selector, key, renderFn, [stateArguments])
key
是可选的。当选择器在thunks兄弟之间不是唯一的时,应该提供它。这确保了当diffing(计算差异)时,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]);
}
这将只在虚拟树中放置一个虚拟的vnode,而不是实际调用numberView
函数。当Snabbdom将这个虚拟的vnode与之前的vnode进行patch时,它会比较n
的值。如果n
的值不变,它将会简单地复用旧的vnode。这将避免了重新创建number视图和diff过程。
这里的视图函数只是一个例子。实际上,只有在渲染需要大量计算时间生成的复杂视图时,thunks才有意义。
虚拟节点文档
属性
- sel
- data
- children
- text
- elm
- key
sel : String
虚拟节点的.sel
属性是在创建过程中传递给h()
的CSS选择器。例如:h('div#container',{},[…])
将创建一个虚拟节点,该节点的.sel
属性为div#container
。
data : Object
虚拟节点的.data
属性用于为模块添加信息,以便在创建实际DOM元素时访问和操作它;比如添加样式、CSS类、属性等。
这个数据对象是h()
的第二个(可选的)参数。
举个例子,h('div', {props: {className: 'container'}}, [...])
将会创建一个虚拟节点,它的.data
对象:
{
"props": {
className: "container"
}
}
children : Array
虚节点的.children
属性是h()
在创建过程中的第三个(可选的)参数。.children
是一个简单的虚拟节点数组,它们会作为子节点被添加到创建的父DOM节点上。
举个例子,h('div', {}, [ h('h1', {}, 'Hello, World') ])
将会创建一个虚拟节点,它的.children
属性:
[
{
sel: 'h1',
data: {},
children: undefined,
text: 'Hello, World',
elm: Element,
key: undefined,
}
]
text : string
当仅使用一个处理文本的子节点创建虚拟节点时,将创建.text
属性,并且只需要使用document.createTextNode()
。
举个例子:h('h1', {}, 'Hello')
将会创建一个虚拟节点,并且它的.text
属性的值为Hello
。
elm : Element
虚拟节点的.elm
属性是指向snabbdom创建的实际DOM节点的指针。这个属性对于在钩子和模块中进行计算非常有用。
key : string | number
当在.data
对象中提供了key,key
属性将会被创建。这个.key
属性用于保存指向以前存在的DOM节点的指针,以避免不必要时重新创建它们。这对于列表重新排序之类的事情非常有用。键必须是字符串或数字,以便进行适当的查找,因为它作为键/值对存储在对象内部,其中.key
是键,值是.elm
属性创建的。
举个例子:h('div', {key: 1}, [])将会创建一个虚拟节点,并且它的.key
属性的值是1
。
结构化应用
Snabbdom是一个底层的虚拟DOM库。它并不限制你应该如何构造你的应用程序。
下面是一些使用Snabbdom构建应用程序的案例。
- functional-frontend-architecture – a repository containing several example applications that demonstrates an architecture that uses Snabbdom.
- Cycle.js – "A functional and reactive JavaScript framework for cleaner code" uses Snabbdom
- Vue.js use a fork of snabbdom.
- scheme-todomvc build redux-like architecture on top of snabbdom bindings.
- kaiju - Stateful components and observables on top of snabbdom
- Tweed – An Object Oriented approach to reactive interfaces.
- Cyclow - "A reactive frontend framework for JavaScript" uses Snabbdom
- Tung – A JavaScript library for rendering html. Tung helps to divide html and JavaScript development.
- sprotty - "A web-based diagramming framework" uses Snabbdom.
- Mark Text - "Realtime preview Markdown Editor" build on Snabbdom.
- puddles - "Tiny vdom app framework. Pure Redux. No boilerplate." - Built with ❤️ on Snabbdom.
- Backbone.VDOMView - A Backbone View with VirtualDOM capability via Snabbdom.
如果您正在以另一种方式使用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.
此错误的原因是在patch之间重用vnode(参见代码示例),为了改进性能,snabbdom将实际的dom节点存储在它的虚拟dom节点中,因此不支持在patch之间重用节点。
var sharedNode = h('div', {}, 'Selected');
var vnode1 = h('div', [
h('div', {}, ['One']),
h('div', {}, ['Two']),
h('div', {}, [sharedNode]),
]);
var vnode2 = h('div', [
h('div', {}, ['One']),
h('div', {}, [sharedNode]),
h('div', {}, ['Three']),
]);
patch(container, vnode1);
patch(vnode1, vnode2);
可以通过创建对象的浅拷贝(这里使用对象扩展语法)来解决这个问题:
var vnode2 = h('div', [
h('div', {}, ['One']),
h('div', {}, [{ ...sharedNode }]),
h('div', {}, ['Three']),
]);
另一个解决方案是将共享的vnode包装在工厂函数中:
var sharedNode = () => h('div', {}, 'Selected');
var vnode1 = h('div', [
h('div', {}, ['One']),
h('div', {}, ['Two']),
h('div', {}, [sharedNode()]),
]);
Activity
[-]Snabbdom 文档翻译[/-][+]Snabbdom 官方文档翻译[/+]