一句话:本导览只管「同一份树数据的多种可视化形态」这一族的选型与踩坑路由——
tree、directory-tree两个组件篇(C09-01、C09-02)加类横写C09-00;范围外一律指向别篇:TreeSelect 的公开属性面与下拉行为在C05-04,TreeTable 的列宽、冻结列与勾选传播在C08-03,虚拟化窗口数学在I08,表格数据变换内核在I06,弹层开合时序在I02。
本类没有「一个内核套两张皮」这回事。两个组件篇共享的抽象只有三件事:把树数据规范化、持有展开(或折叠)集合、算出「哪些节点此刻可见可导航」。规范化上只有 vc-tree 与 vc-tree-select 共享同一套 fieldNames 归一语义,表格的树形展开走的是完全独立的 row 归一(C09-00 论点 1);展开状态的数据结构在三种形态里互不兼容(C09-00 论点 2);「哪些节点参与可见或可导航」则被算了四遍、用途各不同(C09-00 论点 5 那张四行表)。所以选型的第一步不是挑组件,而是挑状态由谁持有。
状态流向按形态分成两条线。树与选择器(目录树算树的变体)把树摊平成一维列表再渲染,展开集合是有序 key 数组,由 vc-tree 持有;选择器形态的展开却不由树内核持有,而由下拉面板自持,内嵌树只是被控组件,优先级是「外部受控 大于 搜索期 大于 面板自持」(C09-00 论点 2、C09-00 六 取舍二)——这一条差异就是 C09-00 坑 7 与坑 8 只在选择器出现、在 Tree 上不出现的根因。表格形态递归渲染组件树,展开是布尔字典且只认字面量 true(C09-00 论点 2、C09-00 坑 2)。两条线的 update:* 事件负载形状都已经是公开 API,因此「换个底层」在技术上可行、在版本上不可行(C09-00 六 取舍五)。
能力边界由「摊平还是递归」这条线一次性划掉。摊平换来虚拟化、展开动画 diff、拖拽定位与缩进连接线,代价是行高必须固定——组件层把 itemHeight 写死成 28(packages/xiaoye-ui/src/tree/Tree.tsx:222),不跟随任何 token(C09-00 论点 3、C09-01 六 虚拟滚动参数)。递归换来单元格插槽自由、行高随内容,代价是分页只切根节点序列、限制不了 DOM 行数(C09-00 坑 10、C08-03 取舍一)。
勾选与半选是全类唯一被真正共享的状态算法,但「共享」的边界要读准:共享的是 conductCheck 这个纯函数(packages/xiaoye-ui/src/vc-tree/utils/conductUtil.ts:203),不是「树实例顺手帮你传导」。选择器为了让内嵌树不重复传导,把内嵌树硬设成 checkStrictly,自己在面板层调这个 util(C05-04 取舍二);表格一侧完全没进这套算法,父子传播写在行组件事件冒泡里、向下写满再向上归算(C08-03 论点 2),按 C09-00 论点 4 的共享范围读,父子传导在这条线上没有入口。三处半选因此各算各的,症状长得像但成因不同(C09-01 坑 2、C09-00 坑 4、C08-03 论点 2)。
第二条风险线在封装层,不在内核。本类大量「传了 props 却不生效」其实有三种不同归因,务必分开查:一是两层默认值——组件层传下去的是已经带默认值的 props 对象,只有值为 undefined 的键才让底层默认生效,同一张声明里能出现两组相反的默认(C09-01 §五 props);二是写在展开之后的同名属性——目录树有八个这种硬覆盖,用户的同名输入从 props 侧进不去(C09-02 论点 1);三是 JSX 顺序覆盖与事件键数组化,选择器侧已实测到 Vue 的 Expected Function, got Array 告警(C05-04 论点 1、C05-04 坑 3)。
最后要认清测试面,因为它决定改动半径。vc-tree/ 与 vc-tree-select/ 两个目录没有任何测试文件,conductCheck 与 flattenTreeData 在全仓测试里零引用(C09-00 §二 的负面结论);组件层 tree/ 唯一的测试只 import 了 calcRangeKeys(C09-01 测试现状);目录树的可执行用例是 8 条(C09-00 §二 测试表,计数口径见 §三 末条)。本类一半的坑后面跟着「该分支无测试保护」,这不是修辞。
| 要解决的问题 | 选它 | 而不是另一个 | 判据 |
|---|---|---|---|
| 页面上摆一棵可交互的树,数据量可能上千行 | C09-01 Tree(走 vc-tree 摊平列表) | C08-03 TreeTable | 只有摊平侧才拿得到虚拟化、展开动画 diff、拖拽定位与缩进连接线(C09-00 论点 3);tree-table 目录里没有任何虚拟化代码(C09-00 六 取舍一) |
| 层级数据要按「行 × 列」呈现,单元格里要放输入框、按钮等任意控件 | C08-03 TreeTable | C09-01 Tree | 递归 <tr> 让每行的 expanded 是局部 computed,行高随内容、插槽自由(C08-03 取舍一);代价是分页只切根、DOM 行数不受约束(C09-00 坑 10) |
| 要在下拉框里选树节点,还要搜索裁剪与「显示父 或 显示子」策略 | C05-04 TreeSelect(形态学对照见 C09-00 论点 2) | 自己把 Tree 塞进弹层 | 选择器自带三层展开优先级与「命中父即整支保留」的搜索裁剪(C09-00 论点 5),内嵌 vc-tree 让节点渲染零重复(C09-00 六 取舍二);自搭弹层这两件都得复刻 |
| 数据是 id 与 pId 的行表,要直接连成树 | C05-04 TreeSelect(第二入口 parseSimpleTreeData 见 C09-00 §二) | C09-01 Tree | 只有选择器形态有 id-pId 入口;库内没有任何 listToTree 类函数承接建树(grep 全仓零命中),走 Tree 就得自己先把行表拼成嵌套数组 |
| 做文件夹浏览:点开即展开、ctrl 或 cmd 追加、shift 连选 | C09-02 DirectoryTree | C09-01 Tree 加自写 onSelect | 选中语义在封装层被整体改写成文件浏览器模型(C09-02 论点 5);接受它就要同时接受上报里 selected 恒真、与 Tree 的 toggle 不连续(C09-02 六),以及一层自持的展开数组(C09-02 论点 4) |
| 需要父子勾选联动,同时子层靠懒加载补齐 | C09-01 Tree | C08-03 TreeTable | 传导算法只在树与选择器两侧共享(C09-00 论点 4),懒加载的装配点也在树侧节点生命周期里(C09-01 论点 4);表格侧既不识别禁用子行、半选会算错(C09-00 坑 4),勾选还是靠行组件事件冒泡逐层上抛(C08-03 论点 2) |
勾选、半选与值形状
checkedKeys 里消失,生产环境零提示 → C09-00 坑 5;同一条数据缺陷在 TreeSelect 上不会丢(选择器自己摘出来再并回,见 C05-04 取舍二),所以严禁用 TreeSelect 的现象推断 Tree。checkStrictly 之后 v-model:checkedKeys 变成对象,includes 与 length 失效、勾选计数恒为 0 → C09-01 坑 2;非受控加严格时实例上的 halfCheckedKeys 恒空是同一条分支的第二坑。C09-00 坑 4(表格侧没有 disabled 概念,也不走 isCheckDisabled)。selectionKeys 里留下「全 false」的空壳条目,Object.keys().length 虚高 → C08-03 坑 3;半选的诞生过程见 C08-03 论点 2。nodeSelect 只发一次,靠事件累积选中集合会少几十条 → C08-03 坑 4;只能改读 update:selectionKeys。展开与折叠
{ 'node-1': 1 } 这类 truthy 字典后「状态像丢了」,点一下反而 emit nodeCollapse → C09-00 坑 2,读取端是严格比较(packages/xiaoye-ui/src/tree-table/TreeTableRow.tsx:57),写入端却是宽松判定。expandedKeys 当初始值、没做回写,用户展开的项被父组件重渲染整体重置 → C09-00 坑 3;Tree 侧的受控判定写法对照 C09-01 §五 受控判定(四组键四种写法)。C08-03 坑 2;同一症状在 I06 坑 5 也登记过,以 C08-03 为准,因为组件视角那篇给出了「组件内没有开关能关掉这次清空」的改法边界。C09-00 坑 1 与 C09-00 坑 9(长度差必须恰好为 1;动画进行中的短路没有排队)。C09-00 坑 7 与 C09-00 坑 8;配套的搜索态懒加载被摘掉见 C05-04 坑 7。expanded-keys 又开 default-expand-all,初值被后者算出又被前者抹掉 → C09-02 坑 1;treeData 异步到达时首屏全收起是同一处初始化的另一面(C09-02 坑 7)。虚拟滚动与数据规模
controlHeightSM 之后虚拟滚动错位、scrollTo({key}) 落点偏 → C09-01 坑 1;组件层写死的 28 与样式侧的 token 没有联系,改法是显式传 itemHeight 而不是改 CSS。同族病灶在引擎侧是 I08 坑 3。ConfigProvider 为 Select 关掉等宽,结果整棵树的虚拟化被一起关掉,DOM 暴涨且没有警告 → C09-01 坑 7;TreeSelect 是这条规则的独立第二份实现(C05-04 坑 8),Select 侧的第三份与 :virtual="false" 的 context 优先级差见 C05-01 坑 5 与 C05-01 坑 10。treeData 里有重复 key,勾一个另一个也变,而且 Tree 不报警 → C09-00 坑 6(库内那个查重函数没有任何调用方);虚拟列表侧的二次后果是行高串行与总高跳动(I08 坑 1)。rows 想限制树表格 DOM,结果 <tr> 数远超预期 → C09-00 坑 10;根因是递归渲染,见 C08-03 论点 1。C09-01 坑 4;目录树侧对应的是图标位没有加载态(C09-02 坑 6)。拖拽
C09-01 坑 5(显示端与执行端判据不同源)。Infinity,drop 位置全落到最后一个可放点 → C09-01 坑 6;几何量只在 dragstart 从隐藏测量节点读一次(C09-01 论点 6)。draggable 传对象或函数不生效 → C09-01 论点 2(组件层把它收窄成布尔,底层三种形态没有合法入口)。外观、前缀与插槽
prefix-cls 后 TreeSelect 下拉里的树整体丢样式 → TreeSelect 侧的拼法断裂(C05-04 坑 1、C05-04 论点 5)。<xy-tree> 写 #checkable 插槽没效果 → C09-01 坑 8(插槽名被组件层无条件占用)。showLine 打开后自定义 switcherIcon 对叶子无效 → C09-01 坑 3;要覆盖叶子得同时用 #leafIcon。icon 关掉或换成空图标,被 getIcon 兜回来 → C09-02 坑 2;三态图标的写死处与「只读 isLeaf 与 expanded」见 C09-02 论点 2。C09-02 坑 3(类名被生成但全仓没有样式匹配它,C09-02 论点 3)。键盘、焦点与暴露面
treeRef.value?.scrollTo 之外的实例方法首帧读到 undefined,或调用方绕过组件层直接摸底层 → C09-01 §五 expose(三层转发见 C09-02 §五);TreeSelect 侧断链的是 scrollTo 本身(C05-04 坑 5)。@drop、@load、@scroll 拿到的是树自己的载荷而不是原生事件 → C09-01 §五 事件(二十多个回调全走 props 通道)。计数与口径(需要仲裁时用)
C09-00 §二 测试表的 8 为准:现 grep 判据 grep -cE "^ it\(" packages/xiaoye-ui/src/directory-tree/__tests__/directory-tree.test.tsx 得 7,再加 packages/xiaoye-ui/tests/shared/mountTest.js 展开的 1 条共 8;C09-02 测试现状写「6 个 it」,但它自己列出的断言已有 7 项,属漏计。顺带一条假阳性陷阱:朴素写 grep -c "it(" 同一文件会得 8,因为该文件第 86 行的 tree.vm.$emit( 里含 it( 子串。C09-01 论点 1(组件层只做改名、补默认值、注入渲染器、二次 emit)、C09-01 §五 props(两层默认值谁生效)、C09-01 论点 2(收窄成布尔的先例)——这三条合起来告诉你「声明比实现窄」在本类没有任何自动校验。C09-00 论点 2(三种表示)加 C09-00 六 取舍一与取舍五(为什么没统一、递归结构为什么答不出「第 N 行落在哪个节点」),虚拟侧再补 I08 论点 1 与 C08-03 取舍一(要先推翻「折叠子树不存在」这个前提)。conductCheck 的行为(例如加 keepMissing):C09-00 六 取舍四(返回值类型只有 Key[] 两个字段,加外来值会让语义分裂)、C09-00 坑 5(树丢、选择器留)、C05-04 取舍二(选择器其实调了两到三遍)。vc-tree 的节点视图或连接线类名:C09-00 §七 改动安全边界第 1、3 条(一次改动同时命中 Tree、DirectoryTree、TreeSelect 三处,以及 conductUtil 的三个消费点),再读 C09-01 §七 改动安全边界(iconUtil.tsx 被 TreeSelect 复用)。C05-04 坑 4(treeMotion 被硬置 null,且 null 不等于「不传」)、C09-00 坑 1(动画本身只在长度差为 1 时成立)、C09-01 坑 1(虚拟估高与真实行高的耦合)。C09-02 论点 5(文件浏览器模型与 selected 恒真)、C09-02 论点 4(自持展开数组、defaultExpandAll 被上提)、C09-02 论点 1(八个硬覆盖属性),图标侧加 C09-02 论点 2 与 C09-01 论点 3。directory-tree/,tree/DirectoryTree.tsx 只是 7 行 re-export 垫片,公开 TreeNode 实际是 vc 组件——两处「名不副实」见 C09-01 论点 7 与 C09-00 §二 源码地图。C09-00 §七 末(tree-table、directory-tree、table-core、tree/util.test.js 四个文件加 pnpm typecheck);注意 vc-tree 与 vc-tree-select 没有用例,全绿不代表没改坏(C09-00 §二、C05-04 坑 9);动过组件目录还要按 AGENTS.md 重新生成导出物。