直接递归遍历DOM树不能生成虚拟DOM,因为虚拟DOM是语义化的JavaScript对象结构,需精简字段、过滤文本节点、转换事件与样式,并保证children顺序、key唯一性及类型一致性。

为什么直接递归遍历 DOM 树不能直接生成虚拟 DOM
因为虚拟 DOM 不是 DOM 树的副本,也不是 HTML 字符串的解析结果——它是一个由开发者或框架控制的、带语义的 JavaScript 对象结构。你用 document.body.children 递归拿到的是真实 DOM 节点,含大量浏览器私有属性(如 offsetTop、getBoundingClientRect),而虚拟节点(VNode)只保留 tagName、props、children、key 等必要字段。
常见错误是写个递归函数把 node.tagName 和 node.textContent 拼成对象,却忽略:
- 文本节点和元素节点混在一起时,
node.childNodes包含空格、换行等 Text Node,需过滤或归一化 - 内联事件(如
onclick)在真实 DOM 上是函数引用,但 VNode 中必须转为字符串或绑定描述符(如{ on: { click: handler } }) - style 属性在真实 DOM 是
CSSStyleDeclaration对象,VNode 中应为{ color: 'red', fontSize: '14px' }这样的 plain object
如何从 HTML 字符串安全生成可 diff 的 VNode 树
不要用 DOMParser 或 innerHTML + 遍历的方式“还原”虚拟节点——那只是逆向工程,丢失 JSX/h 函数的运行时语义,也无法支持动态 key、scoped slots 等关键能力。
正确路径是绕过 HTML 字符串,直接构造 VNode:
立即学习“前端免费学习笔记(深入)”;
Orderly React SDK 钩子使用参考指南,包括 useOrderEntry、usePositionStream、useOrderbookStream、useCollateral 等。
- 用框架提供的编译器(如
@vue/compiler-dom)把模板编译为render()函数,返回 VNode - 手写
h()调用链:例如h('div', { class: 'box' }, [h('p', 'hello')]),输出的是标准 VNode 对象 - 若必须从已有 HTML 入口,先用
DOMParser解析为 Document,再用自定义 walk 函数提取结构,但 props 映射要手动做(如class→className,for→htmlFor)
递归优化 DOM 树的真实价值在哪
递归本身不优化性能,优化的是**操作意图的表达清晰度**。比如树形菜单展开、富文本序列化、服务端 HTML 片段预处理,这些场景需要严格保序的嵌套结构,而非扁平化节点列表。
典型可用场景:
- WYSIWYG 编辑器导出内容 → 递归拆解为
{ markup: '<b>' }</b>/{ text: 'hello' }/{ markup: '' }交错数组 - 服务端渲染(SSR)中,对组件 VNode 树做静态提升(static hoist)前,需递归识别纯静态子树
- diff 算法里,
patch函数本身就是递归的:比较根节点 → 若 type 相同则递归比 children,否则整块替换
虚拟 DOM 的 diff 不依赖 HTML 结构,但依赖递归一致性
diff 算法不看标签名是否匹配 HTML 规范,只比对 VNode 的 tagName、key、props 和 children 结构。所以你手写的递归生成逻辑,必须保证:
- 同一层级的子节点数组顺序与真实 DOM 完全一致(不能因过滤 whitespace 而错位)
- key 必须稳定且唯一,否则列表更新时会复用错误节点
- children 类型统一:字符串 → 文本节点,数组 → 子 VNode 列表,null/undefined → 空内容
最容易被忽略的是:VNode 的 children 是「输入结构」,不是「DOM 子节点快照」。哪怕你递归遍历了真实 DOM,只要没按框架约定组织 children(比如把注释节点当普通文本塞进去),后续 patch 就会出错或跳过渲染。


















