news 2026/9/28 3:29:30

Domphy keyed 实现深度解析:基于 toState 的细粒度 patch-based UI 运行时(js-framework-benchmark)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Domphy keyed 实现深度解析:基于 toState 的细粒度 patch-based UI 运行时(js-framework-benchmark)
  • 性能测试
  • 开发者工具

【免费下载链接】js-framework-benchmark

A comparison of the performance of a few popular javascript frameworks

项目地址:https://gitcode.com/gh_mirrors/js/js-framework-benchmark
点击查看免费下载

导读

本文围绕 js-framework-benchmark 仓库中 frameworks/keyed/domphy 的 keyed 实现展开,系统讲解 Domphy 这一"无 JSX、无虚拟 DOM"的 patch-based 通用 UI 运行时在真实基准场景中的惯用法:如何用单个toState状态驱动整张表格、如何为每行建立独立标签状态、如何通过元素描述符(element descriptor)缓存让重排/删除时未变化的行跳过 patch。读完本文,你将掌握 Domphy 的核心状态模型、keyed 协调器的工作原理,以及如何在 js-framework-benchmark 框架中完成一次可复现的基准构建与运行。

一、Domphy 是什么:README 中的核心定位

仓库中的 frameworks/keyed/domphy/README.md 给出了 Domphy 的一锤定音式定位:

Domphy is a patch-based, framework-agnostic UI runtime. No JSX, no virtual DOM: UIs are plain objects keyed by HTML tag and reactivity is listener-based (toState).

这段描述可以拆解为四个关键事实:

  • Patch-based(基于补丁):Domphy 直接在真实 DOM 上执行最小化的补丁操作,而不是像 React/Vue 那样先构建虚拟 DOM 树再 diff;
  • Framework-agnostic:它不绑定任何 JSX 或模板编译链,视图就是普通的 JavaScript 对象字面量;
  • UI 是"以 HTML 标签为 key 的普通对象":例如{ tr: [...], class: ... }即描述一个<tr>元素,对象属性名(tr、td、a、span)直接映射到标签名,属性值即子节点或属性;
  • 响应式基于监听器:状态由toState(...)创建,读取时建立依赖(订阅),写入时触发订阅者重新执行对应的补丁逻辑。

此外,README 还明确了该实现采用"框架惯用的细粒度风格(idiomatic fine-grained style)",并列出四条设计要点,这四条要点在 src/main.ts 中均有对应实现,是理解整篇文章的骨架:

  1. 用一个keyed 的State<Row[]>承载行列表,每行的_key驱动 keyed 协调器;
  2. 每行拥有独立的 label 状态,使"更新每第 10 行"只重渲染这些行的文本;
  3. 一个表格级别的selectedid 状态,每行从它派生出dangerclass;
  4. 每行的元素描述符只创建一次并跨列表重渲染复用,因此重排/删除时未变化的行会被跳过。

二、目录与构建管线:从 TypeScript 源码到 dist/main.js

该实现的完整目录结构如下:

frameworks/keyed/domphy/ ├── README.md # 实现说明(本文所依据的核心文档) ├── index.html # 基准页面外壳 ├── package.json # 依赖与构建脚本 ├── package-lock.json └── src/ └── main.ts # 全部实现代码(唯一源码入口)

从 package.json 可以看到:

  • 运行时依赖@domphy/core(版本^0.21.1),package.json中的js-framework-benchmark.frameworkVersionFromPackage指向它,基准结果中的版本号domphy-v0.21.1-keyed即由此推导;
  • 开发依赖esbuild(版本0.25.12)负责打包;
  • 项目被标记为 TypeScript 语言实现,main指向dist/main.js。

README 给出的构建命令是两步:

npm install npm run build-prod

其背后对应package.json的两个脚本:

"dev": "esbuild src/main.ts --bundle --format=iife --outfile=dist/main.js", "build-prod": "esbuild src/main.ts --bundle --minify --format=iife --outfile=dist/main.js"

二者的差别仅在于build-prod增加了--minify。两个脚本共同点:--bundle将@domphy/core依赖内联进单一文件,--format=iife生成可直接以<script>标签加载的立即执行函数,输出到dist/main.js。这正是 index.html 中<script src='dist/main.js'></script>所引用的产物,页面只需一个#main挂载点。

三、状态模型:一个列表 State + 一个选中 State

在 src/main.ts 中,全部应用状态只有两个:

const data = toState<Row[]>([]); const selected = toState<number | null>(null);
  • data是带 key 的列表状态,元素类型为Row;
  • selected是表格级别的选中行 id 状态,初始为null。

Row的定义(src/main.ts)值得注意——它把"行 id"与"标签状态"分开:

interface Row { id: number; label: ReturnType<typeof toState<string>>; }

每行内部嵌套一个独立的label: toState<string>(...),这正是 README 第 2 条"per-row label states"的直接体现。buildData(src/main.ts)在创建行时,用三组词表(形容词、颜色、名词)随机拼接生成标签文本,并递增全局nextId作为行 id。

四、六个基准操作:每个操作如何触碰状态

js-framework-benchmark 的 keyed 基准要求实现 6 类操作,main.ts中每个操作都极其精简,因为它们只是对两个状态的读写:

操作函数实现要点(src/main.ts 行号)
Create 1k / Create 10krun(count)selected.set(null)后整体替换data.set(buildData(count))(L115-L118)
Append 1kadd(count)取当前数组 concat 新数据后再整体 set(L120-L122)
Update every 10thupdate()步长 10 遍历,只对rows[i].label调用set(label + " !!!")(L124-L130)
Clearclear()清空选中态与数据(L132-L135)
Remove oneremove(row)若删除的是选中行先清selected,再 filter 掉该行(L137-L140)
Swap rowsswapRows()行数少于 999 直接返回,交换next[1]与next[998]后整体 set(L142-L150)

这里最值得展开的是update():它没有触碰data列表,而是逐行修改每行内嵌的label状态。由于每行元素描述符中的标签文本是(l: any) => row.label.get(l)(见下文第五节),只有被修改的 100 行(每第 10 行)对应的订阅者会被触发并执行 DOM 文本补丁,其余 900 行完全不参与,这正是 README 强调的"update every 10th row re-renders only those rows' text"。

同理,remove()与swapRows()走的是列表整体替换路径,此时 keyed 协调器依据每行的_key计算差异(见第五节),而不是粗暴地重建整个<tbody>。

五、元素描述符:一次创建、跨重渲染复用

README 第 4 条是这套实现最精巧的部分:"row element descriptors are created once per row and reused across list re-renders, so unchanged rows skip patching on reorder/remove."

在 Domphy 中,视图就是普通对象。例如删除按钮图标被定义为模块级常量(src/main.ts):

const REMOVE_ICON: DomphyElement = { span: null, class: "glyphicon glyphicon-remove", "aria-hidden": "true", } as DomphyElement;

每行的元素描述符则由elementFor(row)(src/main.ts)构建,并用一个WeakMap<Row, DomphyElement>(elementCache)缓存:

const elementCache = new WeakMap<Row, DomphyElement>(); function elementFor(row: Row): DomphyElement { let element = elementCache.get(row); if (element) return element; element = { tr: [ { td: row.id, class: "col-md-1" }, { td: [ { a: (l: any) => row.label.get(l), onClick: () => selected.set(row.id), }, ], class: "col-md-4", }, { td: [ { a: [REMOVE_ICON], onClick: () => remove(row), }, ], class: "col-md-1", }, { td: null, class: "col-md-6" }, ], _key: row.id, class: (l: any) => (selected.get(l) === row.id ? "danger" : ""), } as DomphyElement; elementCache.set(row, element); return element; }

这段代码同时示范了 Domphy 描述符的全部形态:

  • 标签即 key:tr、td、a、span直接作为对象键;
  • 静态文本/子元素直接写:{ td: row.id, class: "col-md-1" }表示单元格内容是静态的 id 文本;
  • 动态内容用函数:a: (l: any) => row.label.get(l)是一个读取表达式——它在首次求值时建立label状态与 DOM 文本节点的订阅关系,之后label.set(...)只触发这一处补丁;参数l是 Domphy 在求值时传入的上下文引用;
  • 事件直接挂在描述符上:onClick: () => selected.set(row.id);
  • _key驱动 keyed 协调器:_key: row.id让列表 diff 时能够精确匹配新旧行。注意行 id 与 label 状态都封装在Row对象里,因此即使data.set()传入全新数组,只要Row对象引用不变,elementCache仍能命中同一个描述符,未变化的行在重排/删除时直接被协调器跳过;
  • 派生 class 也用读取表达式:class: (l: any) => (selected.get(l) === row.id ? "danger" : "")——选中状态变化时,只有相关行的 class 补丁会被执行。

elementCache选用WeakMap也很有讲究:key 是Row对象,当行从列表移除且无其他引用时,其描述符可被垃圾回收,避免长期占用内存,这对内存类基准(见第七节)是有利的设计。

六、页面脚手架与挂载

页面结构与 krausest 系列实现的 index.html 保持一致(注释中明确说明 DOM 结构 mirror 自frameworks/keyed/vanillajs):

tr > td.col-md-1 (id) + td.col-md-4 > a (label) + td.col-md-1 > a > span.glyphicon-remove + td.col-md-6 选中行:tr.danger

src/main.ts 的actionButton(id, label, onClick)是操作按钮的工厂函数,它把按钮放进col-sm-6 smallpad容器,并挂上基准约定的元素 id(run、runlots、add、update、clear、swaprows)——这些 id 是 webdriver-ts 驱动脚本点击按钮的定位依据。

顶层App描述符(src/main.ts)由三部分组成:jumbotron 标题区、工具栏按钮区,以及核心的表格:

{ table: [ { tbody: (l: any) => data.get(l).map(elementFor), id: "tbody", }, ], class: "table table-hover table-striped test-data", },

tbody的内容是列表读取表达式:data.get(l).map(elementFor)。列表状态每次set后,这个表达式会重新求值,Domphy 的 keyed 协调器依据每个描述符的_key与旧列表做最小化 diff——新增行插入、删除行移除、重排行移动,其余行保持原 DOM 节点不动。

最后一行完成挂载:

new ElementNode(App).render(document.getElementById("main")!);

src/main.ts 使用@domphy/core导出的ElementNode将App描述符渲染进 index.html 中的<div id='main'></div>。页面还通过<link href="/css/currentStyle.css">引入仓库根目录 css/currentStyle.css,保证 Bootstrap 风格表格与按钮的样式与基准一致。

七、基准中的参与情况与产物规模

在仓库的 webdriver-ts/results.json 中,可以确认该实现以domphy-v0.21.1-keyed的名义参与了基准测试,共覆盖 15 个 benchmark 条目:

01_run1k, 02_replace1k, 03_update10th1k_x16, 04_select1k, 05_swap1k, 06_remove-one-1k, 07_create10k, 08_create1k-after1k_x2, 09_clear1k_x8, 21_ready-memory, 22_run-memory, 25_run-clear-memory, 41_size-uncompressed, 42_size-compressed, 43_first-paint

从中可以观察到基准对实现的两类要求:

  • 交互类(01-09)逐一对应第五节的 6 个操作,外加 select、replace 与"create-after-clear"等组合场景;
  • 内存类(21/22/25)与体积类(41/42)检验运行时开销与产物大小。

关于产物体积,results.json中记录的两条真实数据可作为参考:

  • 41_size-uncompressed:49.5 KB
  • 42_size-compressed:14.4 KB

这组数据与构建管线直接相关:esbuild 以--bundle --minify将src/main.ts与@domphy/core合并为单文件dist/main.js,压缩后仅 14.4 KB(包含完整运行时与基准应用),侧面印证了 Domphy 作为轻量级 UI 运行时的体量。

八、实现要点小结:为什么这套写法是"idiomatic"

回到 README 开头的四句话,再对照源码,可以总结出这套 keyed 实现的设计闭环:

  1. 状态粒度决定重渲染粒度:列表级data只处理增删排,文本级label只处理内容更新,selected只处理选中样式——三者互不干扰;
  2. 描述符稳定性是性能前提:WeakMap缓存的元素描述符让 keyed 协调器在重排时拥有稳定的引用,避免重建与全量 patch;
  3. 读取表达式即依赖声明:(l) => x.get(l)同时完成"读值"与"订阅"两件事,这正是 Domphy"listener-based reactivity"的编程模型;
  4. 标签对象字面量即 UI:无需 JSX 编译器与虚拟 DOM diff,从 src/main.ts 的App对象到真实 DOM,中间只隔一层ElementNode。

如果要在本地复现并验证本实现,按 README.md 中的说明执行即可:

cd frameworks/keyed/domphy npm install npm run build-prod

随后通过 index.html 加载dist/main.js,即可在浏览器中体验 Create 1,000 rows、Update every 10th row、Swap Rows 等全部基准交互。需要说明的是,dist/main.js为构建产物,仓库并未提交该文件,构建前的目录中仅有src/main.ts作为源码入口。

  • 性能测试
  • 开发者工具

【免费下载链接】js-framework-benchmark

A comparison of the performance of a few popular javascript frameworks

项目地址:https://gitcode.com/gh_mirrors/js/js-framework-benchmark
点击查看免费下载

相关推荐

上一篇:5分钟为OBS添加本地AI字幕:LocalVocal完全指南
下一篇:英雄联盟玩家的终极神器:5分钟上手League Akari免费工具箱

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 3:29:30

3年踩坑总结:不懂代码如何搞定手机网站建设行业现状对比评测

3年踩坑总结:不懂代码如何搞定手机网站建设行业现状对比评测 自己不会代码想做网站,是不是打开浏览器搜“手机网站建设行业现状”,出来的全是云里雾里的术语?别急,咱们今天不聊虚的,直接上干货。我入行十年,见过太多老板因为不懂技术,被忽悠花了几万块做了个烂站,结果连百度收录都没有。 这篇长文,就是帮你把…

作者头像 李华
网站建设 2026/9/28 3:29:11

网站免费推广100种方法保姆级建站教程

网站免费推广100种方法保姆级建站教程 网站做好了没人访问,是不是让你焦虑到失眠?很多老板花了几万块定制了高大上的官网,上线一周后台日志显示访客个位数,这种挫败感比建站失败还难受。其实问题不在技术,而在流量获取逻辑。别急着掏钱投广告, 保姆级建站教程…

作者头像 李华
网站建设 2026/9/28 3:29:08

新手入门必知:app网站制作软件有哪些,告别改需求拖一周

新手入门必知:app网站制作软件有哪些,告别改需求拖一周 改个按钮颜色,建站公司拖了一周才给答复?这种憋屈事儿,做项目的老手都懂。很多 新手入门 做项目,最怕的不是技术难,而是沟通成本高,响应速度慢。其实,与其被动等待外包团队排期,不如自己掌握一套高效的 app网站制作软件有哪些…

作者头像 李华
网站建设 2026/9/28 3:29:03

个人网站可以收费吗与衡水网站制作公司哪家专业对比

个人网站能收费吗 3个实战案例教你搞定备案与安全 备案流程一头雾水,是不是让你想直接放弃?别急,很多新手卡在“个人网站能不能赚钱”和“备案怎么过”这两个死胡同里。我干了十年建站,见过太多人因为不懂规则,网站做了一半被下架,或者因为安全漏洞被黑客打穿。今天不讲虚的,直接拆解 实战案例…

作者头像 李华
网站建设 2026/9/28 3:28:59

FL Chart 仓库开发指南:从架构设计到贡献规范的完整解读

【免费下载链接】fl_chart FL Chart is a highly customizable Flutter chart library that supports Line Chart, Bar Chart, Pie Chart, Scatter Chart, Radar Chart and Candlestick Chart. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/fl/fl_chart 点击查看 免费…

作者头像 李华
网站建设 2026/9/28 3:28:18

2026最新dedecms收费全解析,告别模板丑站只需3步

2026最新dedecms收费全解析,告别模板丑站只需3步 很多独立站长刚接触建站时,最大的痛点就是模板网站太丑且不够用。你花几百块买了个DedeCMS模板,上线后发现布局僵硬、配色俗气,根本体现不出品牌形象。2026最新的市场行情显示,单纯的“套模板”已经无法满足用户对视觉体验和功能定制的高要求。…

作者头像 李华