snabbdom 虚拟DOM源码研究
重要通知
snabbdom非常精妙地实现了Virtual DOM,而Vue直接照搬了snabbdom的核心代码,因此精细化研究snabbdom的价值非常重大。
snabbdom基本概况
snabbdom是一个精简化、模块化、功能强大、性能卓越的虚拟 DOM 库。
代码示例
<section id="container"></section>
import {
Fragment,
array,
attachTo,
attributesModule,
classModule,
datasetModule,
eventListenersModule,
fragment,
h,
htmlDomApi,
init,
jsx,
primitive,
propsModule,
styleModule,
thunk,
toVNode,
vnode,
} from "snabbdom";
const RenderDOM = init([
// 通过传入模块初始化 patch 函数
classModule, // 开启 classes 功能
propsModule, // 支持传入 props
styleModule, // 支持内联样式同时支持动画
eventListenersModule, // 添加事件监听
]);
const container = document.getElementById("container");
const vnode = h("div#container.container", {},
[
h('h1', {
style: { 'text-align': 'center', color: '#f00' }
}, 'snabbdom示例'),
h('p', {
style: { 'text-align': 'center' }
}, '段落'),
"内容描述",
h('br'),
h("a", {
props: {
href: "/foo"
}
}, "超链接"),
h('br'),
h('button', {
style: {},
on: {
click: function() { console.log('提交'); }
}
}, '提交')
]
);
RenderDOM(container, vnode);
snabbdom生态体系
snabbdom-to-html
snabbdom-to-html提供的服务器端 HTML 输出。
snabbdom-helpers
使用snabbdom-helpers创建紧凑的虚拟 DOM。
snabby
使用snabby的模板字符串支持。
snabbdom-looks-like
带有snabbdom-looks-like 的虚拟 DOM 断言。
snabbdom源码结构分析

VNode实现机制
VNode数据结构
function vnode(
sel, // <String>,通过对 h() 传入一个 CSS 选择器生成
data, // <Object>,虚拟节点用于添加 模块 信息以便在创建时访问或操作 DOM 元素、添加样式、操作 CSS classes、attributes 等
children, // <Array>,通过 h() 传入的第三个参数(可选)生成
text, // <string>,当仅使用文本作为子节点并通过 document.createTextNode() 创建虚拟节点时,生成 .text。
elm, // <Element>,指向由 snabbdom 创建的真实 DOM 节点
key, // <string | number>,标识
) {
const key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}
h(sel,b,c)创建VNode
function h(
sel, // [比选] 字符串类型元素标签 | 选择器对象,例如div、h1、button、a等
b, // [可选] 数据对象
c // [可选] 子节点数组或字符串
) {
let data = {};
let children;
let text;
let i;
if (c !== undefined) {
if (b !== null) {
data = b;
}
if (array(c)) {
children = c;
}
else if (primitive(c)) {
text = c.toString();
}
else if (c && c.sel) {
children = [c];
}
}
else if (b !== undefined && b !== null) {
if (array(b)) {
children = b;
}
else if (primitive(b)) {
text = b.toString();
}
else if (b && b.sel) {
children = [b];
}
else {
data = b;
}
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (primitive(children[i]))
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
if (sel[0] === "s" &&
sel[1] === "v" &&
sel[2] === "g" &&
(sel.length === 3 || sel[3] === "." || sel[3] === "#")) {
addNS(data, children, sel);
}
// 生成vnode结构
return vnode(sel, data, children, text, undefined);
}
DOM节点转换为VNode
function toVNode(
node,
domApi
) {
const api = domApi !== undefined ? domApi : htmlDomApi;
let text;
if (api.isElement(node)) {
const id = node.id ? "#" + node.id : "";
const cn = node.getAttribute("class");
const c = cn ? "." + cn.split(" ").join(".") : "";
const sel = api.tagName(node).toLowerCase() + id + c;
const attrs = {};
const dataset = {};
const data = {};
const children = [];
let name;
let i, n;
const elmAttrs = node.attributes;
const elmChildren = node.childNodes;
for (i = 0, n = elmAttrs.length; i < n; i++) {
name = elmAttrs[i].nodeName;
if (name[0] === "d" &&
name[1] === "a" &&
name[2] === "t" &&
name[3] === "a" &&
name[4] === "-") {
dataset[name.slice(5)] = elmAttrs[i].nodeValue || "";
}
else if (name !== "id" && name !== "class") {
attrs[name] = elmAttrs[i].nodeValue;
}
}
for (i = 0, n = elmChildren.length; i < n; i++) {
children.push(toVNode(elmChildren[i], domApi));
}
if (Object.keys(attrs).length > 0)
data.attrs = attrs;
if (Object.keys(dataset).length > 0)
data.dataset = dataset;
if (sel[0] === "s" &&
sel[1] === "v" &&
sel[2] === "g" &&
(sel.length === 3 || sel[3] === "." || sel[3] === "#")) {
addNS(data, children, sel);
}
return vnode(sel, data, children, undefined, node);
}
else if (api.isText(node)) {
text = api.getTextContent(node);
return vnode(undefined, undefined, undefined, text, node);
}
else if (api.isComment(node)) {
text = api.getTextContent(node);
return vnode("!", {}, [], text, node);
}
else {
return vnode("", {}, [], undefined, node);
}
}
sameVnode(oldVnode, vnode)
判断是否为相同vnode的方法,根据key、data、sel三个数据比较。
function sameVnode(vnode1, vnode2) {
var _a, _b;
const isSameKey = vnode1.key === vnode2.key;
const isSameIs = ((_a = vnode1.data) === null || _a === void 0 ? void 0 : _a.is) === ((_b = vnode2.data) === null || _b === void 0 ? void 0 : _b.is);
const isSameSel = vnode1.sel === vnode2.sel;
const isSameTextOrFragment = !vnode1.sel && vnode1.sel === vnode2.sel
? typeof vnode1.text === typeof vnode2.text
: true;
return isSameSel && isSameKey && isSameIs && isSameTextOrFragment;
}
snabbdom初始化init函数
初始化数据
const cbs = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: [],
};
const api = domApi !== undefined ? domApi : htmlDomApi;
for (const hook of hooks) {
for (const module of modules) {
const currentHook = module[hook];
if (currentHook !== undefined) {
cbs[hook].push(currentHook);
}
}
}
emptyNodeAt()
emptyNodeAt(elm):
createElm()
createElm(vnode, insertedVnodeQueue):
addVnodes()
addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue):
invokeDestroyHook()
invokeDestroyHook(vnode):
removeVnodes()
removeVnodes(parentElm, vnodes, startIdx, endIdx):
updateChildren()
updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue):
patchVnode()
patchVnode(oldVnode, vnode, insertedVnodeQueue):
patch()
patch(oldVnode, vnode):
patch实现机制
源码分析
function patch(oldVnode, vnode) {
let i, elm, parent;
const insertedVnodeQueue = [];
for (i = 0; i < cbs.pre.length; ++i)
// 执行pre钩子函数,尤其是styleModule
cbs.pre[i]();
// 如果oldVnode是元素,则转换为vnode对象
if (isElement(api, oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
// 文档片段对象
else if (isDocumentFragment(api, oldVnode)) {
oldVnode = emptyDocumentFragmentAt(oldVnode);
}
// 是否为相同vnode
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
}
else {
elm = oldVnode.elm;
// 获取旧节点父级元素,用于挂载节点
parent = api.parentNode(elm);
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 挂载新节点
api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
// 移除挂载的旧节点
removeVnodes(parent, [oldVnode], 0, 0);
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
}
for (i = 0; i < cbs.post.length; ++i)
cbs.post[i]();
return vnode;
}
diff实现机制
diff算法,即通过对比同一层级节点,如果不相同则直接替换,如果相同则继续对比其子节点,如此往复,则实现新节点与旧节点的diff算法。
触发时机
function patch(oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
}
}
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue);
}
}
}
源码分析
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx;
let idxInOld;
let elmToMove;
let before;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
}
else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
}
else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
}
else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
}
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
}
else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}
else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key];
if (isUndef(idxInOld)) {
// New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
}
else {
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
}
else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined;
api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (newStartIdx <= newEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
}
if (oldStartIdx <= oldEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const hook = (_a = vnode.data) === null || _a === void 0 ? void 0 : _a.hook;
(_b = hook === null || hook === void 0 ? void 0 : hook.prepatch) === null || _b === void 0 ? void 0 : _b.call(hook, oldVnode, vnode);
const elm = (vnode.elm = oldVnode.elm);
if (oldVnode === vnode)
return;
if (vnode.data !== undefined ||
(isDef(vnode.text) && vnode.text !== oldVnode.text)) {
(_c = vnode.data) !== null && _c !== void 0 ? _c : (vnode.data = {});
(_d = oldVnode.data) !== null && _d !== void 0 ? _d : (oldVnode.data = {});
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
(_g = (_f = (_e = vnode.data) === null || _e === void 0 ? void 0 : _e.hook) === null || _f === void 0 ? void 0 : _f.update) === null || _g === void 0 ? void 0 : _g.call(_f, oldVnode, vnode);
}
// 旧节点子集
const oldCh = oldVnode.children;
// 新节点子集
const ch = vnode.children;
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue);
}
else if (isDef(ch)) {
if (isDef(oldVnode.text))
api.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
}
else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
else if (isDef(oldVnode.text)) {
api.setTextContent(elm, "");
}
}
else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
api.setTextContent(elm, vnode.text);
}
(_h = hook === null || hook === void 0 ? void 0 : hook.postpatch) === null || _h === void 0 ? void 0 : _h.call(hook, oldVnode, vnode);
}
Module模块
attributesModule
attributes模块与 props 相同,但是是使用 attr 替代 prop。
- 代码示例
import { h } from 'snabbdom';
h('a', {
attrs: {
href: '/home'
}
}, '');
- 源码分析
const attributesModule = {
create: updateAttrs,
update: updateAttrs,
};
classModule
class模块提供了一种简单的方式来动态配置元素的 class 属性,这个模块值为一个对象形式的 class 数据,对象中类名需要映射为布尔值,以此来表示该类名是否应该出现在节点上。
- 代码示例
import { h } from 'snabbdom';
h('div', {
class: { active: true, container: true }
}, '');
- 源码分析
const classModule = {
create: updateClass,
update: updateClass
};
datasetModule
dataset模块允许在 DOM 元素上设置自定义 data 属性,然后通过 HTMLElement.dataset 来访问这些属性。
- 代码示例
import { h } from 'snabbdom';
h('div', {
dataset: {
id: Date.now()
}
}, '');
- 源码分析
const datasetModule = {
create: updateDataset,
update: updateDataset,
};
eventListenersModule
event模块,即支持事件绑定功能。
- 代码示例
import { h } from 'snabbdom';
h('button', {
on: {
click: function() {}
}
}, '');
- 源码分析
const eventListenersModule = {
create: updateEventListeners,
update: updateEventListeners,
destroy: updateEventListeners,
};
propsModule
props模块允许你设置 DOM 元素的属性。
- 代码示例
import { h } from 'snabbdom';
h('a', {
props: {
href: '/home'
}
}, '');
- 源码分析
const propsModule = {
create: updateProps,
update: updateProps
};
styleModule
style模块用于让动画更加平滑,它的核心是允许你再元素上设置 CSS 属性。
- 代码示例
import { h } from 'snabbdom';
h('div', {
style: {
width: '100px'
}
}, '');
- 源码分析
const styleModule = {
pre: forceReflow,
create: updateStyle,
update: updateStyle,
destroy: applyDestroyStyle,
remove: applyRemoveStyle,
};
钩子hooks
- 源码分析
const hooks = [
"create",
"update",
"remove",
"destroy",
"pre",
"post",
];
snabbdom应用案例
Cycle.js
- 整体架构
- snabbdom设计
Vue.js
- 整体架构
- snabbdom设计
scheme-todomvc
- 整体架构
- snabbdom设计
kaiju
- 整体架构
- snabbdom设计
Tweed
- 整体架构
- snabbdom设计
Cyclow
- 整体架构
- snabbdom设计
Tung
- 整体架构
- snabbdom设计
sprotty
- 整体架构
- snabbdom设计
Mark Text
- 整体架构
- snabbdom设计
puddles
- 整体架构
- snabbdom设计
Backbone.VDOMView
- 整体架构
- snabbdom设计
Rosmaro Snabbdom starter
- 整体架构
- snabbdom设计
Pureact
- 整体架构
- snabbdom设计
Snabberb
- 整体架构
- snabbdom设计
WebCell
- 整体架构
- snabbdom设计