Skip to content

虚拟 DOM(vnode)

一个轻量级、Snabbdom 风格的虚拟 DOM。它把界面表示为普通的 JavaScript 对象(VNode),对新旧 两棵树做 diff,并只把差异应用到真实 DOM 上。

  • init() 构建协调器并返回一个 patch 函数。内置模块(class / props / attrs / style / events)会被自动注册。
  • patch(oldVnode, newVnode) 用于挂载一棵树(当 oldVnode 是真实 DOM 元素时),或对两棵 vnode 树做 diff 并就地更新 DOM。
  • h(sel, dataOrChildren?, children?) 是用于构建 VNode 的 hyperscript 辅助函数。

引入

js
import { init, h, classModule, propsModule, styleModule, eventListenersModule } from 'ranuts/vnode';

注意:在本实现中 init() 不接受任何参数 —— 模块集合是固定的、在内部注册的,因此各个 *Module 导出不需要传给 init。它们被导出仅供参考和查看。

示例

快速上手

js
import { init, h } from 'ranuts/vnode';

// init() 返回一个 `patch` 函数。
// 内置模块(class、props、attrs、style、events)会被自动注册。
const patch = init();

const container = document.getElementById('app');

// 构建一棵 vnode 树
let vnode = h('div#app.container', { style: { color: 'red' } }, [
  h('h1', 'Hello vnode'),
  h('button', { on: { click: () => console.log('clicked') } }, 'Click me'),
]);

// 首次渲染:把一个真实 DOM 元素作为旧 vnode 传入以挂载到它上面
patch(container, vnode);

// 之后:构建更新后的树,并把上一个 vnode patch 成它。
// 只有差异(文本、样式、监听器)会被应用到 DOM 上。
const newVnode = h('div#app.container', { style: { color: 'green' } }, [
  h('h1', 'Hello again'),
  h('button', { on: { click: () => console.log('clicked') } }, 'Updated'),
]);

patch(vnode, newVnode);
vnode = newVnode; // 保留最新的树,供下一次 patch 使用

h 构建节点

js
// 仅标签
h('div');

// 标签 + 数据
h('div', { class: { active: true } });

// 标签 + 单个文本子节点
h('span', 'hello');

// 标签 + 子节点数组
h('ul', [h('li', 'one'), h('li', 'two')]);

// 标签 + 数据 + 子节点
h('a', { attrs: { href: '/home' } }, 'Home');

// CSS 风格的选择器可设置 id 和 class
h('div#main.card.large', 'content'); // <div id="main" class="card large">content</div>

// 当选择器以 "svg" 开头时,会自动应用 SVG 命名空间
h('svg', { attrs: { width: 100, height: 100 } }, [h('circle', { attrs: { cx: 50, cy: 50, r: 40 } })]);

API

init()

创建协调器并返回一个 patch 函数。内置模块在内部注册,它不接受任何参数。

Return

说明类型
patch把 vnode 树挂载/diff 到真实 DOM(oldVnode: VNode | Element, vnode: VNode) => VNode

patch(oldVnode, vnode)

init() 返回。首次调用时,把真实 DOM Element 作为 oldVnode 传入以挂载树。之后调用时, 传入上一个 VNode 以就地做 diff 更新。返回新的 VNode,你需要把它保留为下一次调用的“旧”值。

Parameters

参数说明类型
oldVnode上一个 vnode,或首次挂载时的 DOM 元素VNode | Element
vnode要渲染的新 vnode 树VNode

h(sel, dataOrChildren?, children?)

用于构建 VNode 的 hyperscript 辅助函数。它是重载的:

签名说明
h(sel)仅根据选择器创建元素
h(sel, data)VNodeData 的元素(data 可为 null
h(sel, children)带子节点的元素 —— 文本/数字、单个 VNode 或数组
h(sel, data, children)同时带数据和子节点的元素

Parameters

参数说明类型
selCSS 风格选择器:tagtag#idtag.class、组合(div#id.a.b)。svg… 会自动附加 SVG 命名空间string
data节点数据 —— class / props / attrs / style / 监听器 / key / hook,可为 nullVNodeData | null
children文本或数字(会变成文本节点)、单个 VNode,或它们的数组VNodeChildren

VNodeData 字段

字段说明类型处理模块
props通过 elm[key] = value 设置的 DOM 属性Record<string, any>propsModule
attrs通过 setAttribute 设置的 HTML 属性(true/false 用于开关属性)Record<string, string | number | boolean>attributesModule
class条件类名 —— name → boolean 映射Record<string, boolean>classModule
style内联样式 —— name → value 映射(--var 键使用 CSS 变量)Record<string, any>styleModule
on事件监听器 —— event → handler(或处理函数数组)Record<string, Function | Function[]>eventListenersModule
keydiff 算法用于匹配/重排子节点的稳定标识string | number(diff 核心)
ns命名空间 URI(在 SVG 子树中自动设置)string(diff 核心)
hook单个 vnode 的生命周期钩子(HooksHooks(类型层 —— 见注意)

注意:hookHooks 类型属于公开的类型层。本精简实现通过模块生命周期 (create / update / destroy)驱动 DOM;当前的 patch 流程不会调用单个 vnode 的 data.hook 回调。

模块

每个模块负责 VNodeData 的一部分。init() 会注册全部模块;它们也被单独导出。

导出处理说明
classModuledata.class根据 name → boolean 映射增删 class
propsModuledata.props直接赋值 DOM 属性(elm[key] = value
attributesModuledata.attrs通过 setAttribute 设置/移除 HTML 属性(含 xml/xlink)
styleModuledata.style设置内联样式和 CSS 自定义属性
eventListenersModuledata.on添加/移除事件监听器
modules默认注册表对象,把每个模块名映射到对应模块

较底层的导出

导出类型说明
vnode(sel, data, children, text, elm) => VNodeh 内部使用的底层 VNode 工厂。应用代码中请优先使用 h
addNS(data, children, sel) => void递归地为子树应用 SVG 命名空间。hsvg… 选择器会自动调用它。
htmlDomApiDOMAPIpatch 内部使用的默认浏览器 DOM 适配器(创建/插入/移除/文本节点等)。
is{ array, isStr, primitive, isVnode }vnode 内部使用的一组类型守卫辅助函数。
Chainclass Chain一个链式命令式 DOM 构建器(setAttributeappendsetTextContent 等),独立于 vnode diff。
create(tagName, options?) => Chain返回新 Chain 的便捷工厂。

类型

类型结构 / 含义
VNode{ sel, data, children, elm, text, key, listener? } —— 一个虚拟节点
VNodeData{ props?, attrs?, class?, style?, on?, key?, ns?, hook? } —— 见上文字段
VNodesVNode[]
VNodeChildElementVNode | string | number
VNodeChildrenVNodeChildElement | VNodeChildElement[]
ArrayOrElement<T>T | T[]
Keystring | number
Hooks{ pre?, init?, create?, insert?, prepatch?, update?, postpatch?, destroy?, remove?, post? }
DOMAPI描述 patch 所用 DOM 操作的接口(参见 htmlDomApi
Fragment用于片段处理的 DocumentFragment 扩展
ModulesRecord<string, Record<string, ModuleHook>> —— 模块注册表的形状
ModuleHook单个模块生命周期回调

注意事项

  1. 仅限浏览器。 ranuts/vnode 会访问 document 和 DOM API;请在浏览器代码中引入,而非 Node 中。
  2. 保留上一个 vnode。 patch 返回新的 VNode。请把它保存下来,在下一次更新时作为 oldVnode 传入,这样 diff 才会基于当前的树来计算。
  3. textchildren 互斥 —— 一个节点要么是文本节点,要么是带子节点的元素。
  4. 列表请使用 key 渲染动态列表时,给同级节点设置稳定的 key 值,让 diff 能够匹配并 重排节点,而不是重新创建它们。

Released under the MIT License.