递归 Patch 是按需、可控、带终止条件的子节点比对,仅当新旧 vnode 同类型且 children 非 null/undefined 时触发;开销源于重复 key 查找、无意义 vnode 构建和空分支遍历;切断无效递归需显式设 children 为 []、render 守卫 isExpanded、patch 前判空及路径级 key。

“递归 Patch”不是指无条件层层深入子树,而是指虚拟 DOM 的 patch 函数在比对节点时,按需、可控、带终止条件地向下进入 children 层级。它本质是 diff 算法在 vnode 树结构上的执行路径——看似递归,实则被 key、状态、空数组等多重机制约束,避免无效穿透。
递归 Patch 的真实触发条件
patch 不会盲目递归。只有当以下全部满足时,才会进入子节点比对:
- 新旧 vnode 是同类型节点(sameVnode 判断通过:tag 相同、key 相同、data 结构兼容)
- 双方都存在 children 字段(哪怕为空数组 [])
- children 不是 null / undefined(null 表示无子节点,直接跳过;[] 表示有子结构但当前为空)
垂直递归的开销来源在哪?
真正拖慢 patch 的,从来不是“递归调用本身”,而是递归过程中反复发生的三类操作:
- 重复 key 查找:未建立 key → index 映射表时,每次新增/移动节点都要遍历旧 children 数组找匹配项
- 无意义 vnode 构建:收起状态仍生成完整子 vnode 树(含 label、icon、事件绑定),但后续 patch 又全跳过——浪费内存与 CPU
- 深度遍历空分支:某节点 isExpanded = false,其 children 为 [],但 patch 未做 early return,仍调用 updateChildren 或遍历空数组
如何切断无效垂直递归?
关键不在禁用递归,而在让递归“知情”且“有边界”:
- 折叠节点的 children 必须显式设为 [],而非 null 或 undefined —— patch 才能识别“结构存在但内容为空”,从而跳过子比对逻辑
- renderNode 中对 isExpanded 做守卫:false 时直接返回 [],不调用子级 renderNode,从源头减少 vnode 创建量
- patch 函数在进入 updateChildren 前加判断:
if (newCh.length === 0 && oldCh.length === 0) return - 为每个节点生成 key 时拼入层级路径(如
id + '-level-2'),避免跨层 key 冲突导致误复用或暴力重排
一个典型误操作对比
❌ 错误做法:展开/折叠时重绘整棵树 → 每次触发从根开始 full-depth 递归 patch,即使 90% 节点未变
✅ 正确做法:仅更新目标节点 isExpanded,其 vnode 的 children 字段由 [] ↔ [vnodeA, vnodeB] 切换 → patch 仅比对该 vnode 的 children 引用变化,复用其余所有属性,子树 patch 自动被截断

















