#8218·naive-ui

[Bug] 2.45.x NTree/NSpin 行批量卸载/重建时 patch/unmount 空引用崩溃(2.44.1 正常)

Author: vcqrCreated Sep 17, 2026Updated Sep 17, 2026

环境

  • naive-ui:2.45.3(回归从 2.45.0 开始;2.44.1 完全正常)
  • vue:3.5.42
  • 浏览器:Chromium(Tauri WebView2 与 Chrome 均可复现)
  • 场景:主机管理树(数百节点),搜索框逐字符过滤触发

现象

NTree 因搜索过滤导致节点成批卸载/重建时(每次按键 fNodes 变化,或清空搜索还原整树),Vue 的 patch/unmount 阶段抛出一系列 null 引用 TypeError,补丁中断、树视图冻结。

我们先后做了三层规避,全部仍然崩溃:

  1. 移除外层 NSpin(崩溃归因从 Spin 变为 anonymous 组件);
  2. 移除 virtual-scroll(传统树渲染);
  3. 弃用 pattern/filter/show-irrelevant-nodes,改为应用层数据过滤(直接替换 :data 为过滤后的新数组)。

错误家族始终不变,归因组件随移除依次变化(Spin → anonymous),说明问题在 2.45 新编译产物内部的 vnode 引用管理,而非某个具体用法组合。

错误签名(五类,均已稳定捕获)

  1. patch 阶段,旧组件 vnode 已失效(component 为 null):
javascript
TypeError: Cannot read properties of null (reading 'emitsOptions')
    at shouldUpdateComponent
    at updateComponent
    at processComponent
    at patch
    at patchBlockChildren
    at patchElement
    at processElement
    at patch
    at ReactiveEffect.componentUpdateFn
  1. patch 阶段,行元素 DOM 已被拆除:
javascript
TypeError: Cannot set properties of null (setting '__vnode')      // patchElement: el.__vnode
TypeError: Cannot read properties of null (reading 'insertBefore') // mountElement → insert
    at insert
    at mountElement
    at processElement
    at patch
    at patchBlockChildren
    at processFragment
    at patch
    at patchBlockChildren
    at patchElement
  1. unmount 阶段,children 数组中出现 null vnode:
javascript
TypeError: Cannot read properties of null (reading 'type')
    at unmountComponent
    at unmount
    at unmountChildren
    at unmount (×N)
    at unmountComponent
    at unmount
  1. patch 阶段,实例未完成挂载:
javascript
TypeError: Cannot read properties of null (reading 'subTree')
    at getNextHostNode
    at patch
    at patchBlockChildren
    at patchElement
    at processElement
    at patch
    at ReactiveEffect.componentUpdateFn
  1. 报错组件:最初为 Spin,移除 NSpin 后为 anonymous(无名字组件,疑似 vue-jsx-vapor/vdom.mjs 中的 renderList 包装 defineComponent),随后大量为树行组件 TreeNodeContent / anonymous

分析(结合堆栈与 2.45 产物代码的推测,供排查方向参考)

2.45.0 起组件经 vue-jsx-vapor/vdom.mjs 编译,怀疑与以下机制在"批量卸载/重建"下的表现有关:

  • normalizeVNodecreateBlock(cloneIfMounted(value)):cloneIfMountedchild.el === null 判定是否原样复用 vnode,同一次 flush 内同一 vnode 被多处引用时可能绕过克隆;
  • 匿名 defineComponent + renderList(patchFlag 128 keyed Fragment)包装的列表渲染;
  • 组件 vnode 的 dynamicChildren 携带 slot 渲染产物,批量变更时 block 元数据长度不稳定,Vue patchElement 走 "Force full diff" 路径,期间旧 vnode 的 component/el 已失效 → 后续 patch/unmount 踩空。

复现要点

真实应用中稳定复现(tauri dev 与浏览器均),要点:

  • NTree 数百节点,搜索框逐字符输入,每键改变树数据(无论 naive 的 pattern 过滤,还是应用层数据过滤后替换 :data);
  • 清空搜索、整树还原的瞬间最容易触发(卸载/挂载量最大);
  • 时序相关:数据量大、输入快时必现;小数据量偶发。暂未收缩出独立最小 demo,如需要我们可以继续提供更多信息。

临时缓解

固定 naive-ui 2.44.1(重写前最后一版,产物中无 vue-jsx-vapor),已稳定运行,功能无差异。