@wxwzl/vue-virtual-tree
Vue Virtual Tree
一个基于自研虚拟列表(@wxwzl/vue-virtual-list)的高性能 Vue 3 虚拟树列表组件库,功能参照 Element Plus Tree 组件。
特性
- ⚡️ 虚拟滚动 - 支持百万级节点的高性能渲染(分片初始化,主线程不卡顿)
- 🌳 完整树功能 - 展开/折叠、选择、过滤、拖拽等
- 🎨 Element Plus 风格 - API 设计与 Element Plus Tree 保持一致
- 📦 TypeScript - 完整的类型支持
- 🔧 灵活配置 - 支持自定义字段映射、节点内容等
在线demo
安装
pnpm add @wxwzl/vue-virtual-tree
快速开始
<template>
<VirtualTree :data="treeData" :height="400" />
</template>
<script setup lang="ts">
import { VirtualTree } from "@wxwzl/vue-virtual-tree";
import "@wxwzl/vue-virtual-tree/style";
const treeData = [
{
id: 1,
label: "节点 1",
children: [
{ id: 2, label: "节点 1-1" },
{ id: 3, label: "节点 1-2" },
],
},
];
</script>
加载状态
<template>
<!-- 使用默认加载状态 -->
<VirtualTree :data="treeData" :loading="isLoading" :height="400" />
<!-- 自定义加载状态 -->
<VirtualTree :data="treeData" :loading="isLoading" :height="400">
<template #tree-loading>
<div class="custom-loading">数据加载中...</div>
</template>
</VirtualTree>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { VirtualTree } from "@wxwzl/vue-virtual-tree";
const isLoading = ref(true);
const treeData = ref([]);
// 模拟数据加载
setTimeout(() => {
treeData.value = [
{ id: 1, label: "节点 1" },
{ id: 2, label: "节点 2" },
];
isLoading.value = false;
}, 2000);
</script>
功能
基础功能
- ✅ 树形结构展示
- ✅ 节点展开/折叠
- ✅ 虚拟滚动(支持大量数据)
- ✅ 节点点击事件
选择功能
- ✅ 单选模式
- ✅ 多选模式(复选框)
- ✅ 节点选中状态管理
- ✅ 父子节点关联选择
数据功能
- ✅ 懒加载节点
- ✅ 节点过滤/搜索
- ✅ 默认展开节点
- ✅ 默认选中节点
交互功能
- ✅ 自定义节点内容(插槽)
- ✅ 自定义节点类名
- ✅ 节点拖拽排序
- ✅ 手风琴模式(同级节点互斥展开)
高级功能
- ✅ 节点禁用
- ✅ 节点图标自定义
- ✅ 节点加载状态
- ✅ 空状态展示
API
Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| data | 树形数据 | TreeNodeData[] |
[] |
| props | 数据字段映射配置 | TreePropsConfig |
{} |
| show-checkbox | 是否显示复选框 | boolean |
false |
| check-strictly | 是否严格遵循父子不关联 | boolean |
false |
| default-expand-all | 是否默认展开所有节点 | boolean |
false |
| default-expanded-keys | 默认展开的节点 key 数组 | (string | number)[] |
[] |
| default-checked-keys | 默认选中的节点 key 数组 | (string | number)[] |
[] |
| current-node-key | 当前选中的节点 key | string | number |
- |
| expand-on-click-node | 是否点击节点展开 | boolean |
true |
| lazy | 是否懒加载 | boolean |
false |
| load | 懒加载方法 | Function |
- |
| filter-node-method | 节点过滤方法 | Function |
- |
| accordion | 是否手风琴模式 | boolean |
false |
| draggable | 是否可拖拽 | boolean |
false |
| allow-drop | 拖拽时判断是否允许放置 | Function |
- |
| allow-drag | 拖拽时判断节点是否允许拖拽 | Function |
- |
| item-size | 节点高度(固定高度时使用) | number |
32 |
| height | 虚拟滚动容器高度 | number | string |
'100%' |
| indent | 每一级节点的缩进值,支持数字或函数(函数入参为 node: FlatTreeNode) |
number | (node) => number |
18 |
| loading | 是否显示加载状态 | boolean |
false |
| fixed-height | 固定行高模式。true 时行高恒等于 item-size:O(1) 滚动定位、无高度测量,大数据量下快速滚动不白屏;此时超长 label 不换行(省略号截断) |
boolean |
false |
| buffer | 虚拟滚动缓冲区(px) | number |
500 |
Events
| 事件名 | 说明 | 参数 |
|---|---|---|
| node-click | 节点点击 | (data, node, event) |
| node-check | 节点复选框点击 | (data, checked) |
| node-expand | 节点展开 | (data, node) |
| node-collapse | 节点折叠 | (data, node) |
| current-change | 当前选中节点变化 | (data, node) |
| node-drag-start | 拖拽开始 | (node, event) |
| node-drag-enter | 拖拽进入 | (draggingNode, event, node) |
| node-drag-leave | 拖拽离开 | (draggingNode, event, node) |
| node-drag-over | 拖拽悬停 | (draggingNode, event, node) |
| node-drag-end | 拖拽结束 | (draggingNode, event) |
| node-drop | 拖拽放置 | (draggingNode, dropNode, dropType, event) |
Methods
| 方法名 | 说明 | 参数 |
|---|---|---|
| getCheckedNodes | 获取选中的节点 | (leafOnly?, includeHalfChecked?) |
| getCheckedKeys | 获取选中的节点 key | (leafOnly?) |
| setCheckedNodes | 设置选中的节点 | (nodes, leafOnly?) |
| setCheckedKeys | 设置选中的节点 key | (keys, leafOnly?) |
| getCurrentNode | 获取当前选中节点 | - |
| setCurrentNode | 设置当前选中节点 | (node) |
| getCurrentKey | 获取当前选中节点的 key | - |
| setCurrentKey | 设置当前选中节点的 key | (key) |
| filter | 过滤节点 | (value) |
| getNode | 根据 key 获取节点 | (key) |
| remove | 删除节点 | (key) |
| append | 追加节点 | (data, parentKey?) |
| insertBefore | 在节点前插入 | (data, key) |
| insertAfter | 在节点后插入 | (data, key) |
| updateKeyChildren | 更新节点的子节点(懒加载) | (key, data) |
| replace | 替换指定节点的数据(详见下文) | (data, key) |
| scrollToNode | 滚动到指定节点 | (key, options?) |
replace 方法说明
replace(data, key) 用于节点级局部更新,替换指定节点的数据:
data中不包含children时:保留原子树,原地更新节点数据(O(1) 视图刷新,不重建整树)data中包含children时:整体替换该节点的子树(走全量重建)- 新数据的 key 与原 key 不同时:自动迁移该节点的展开 / 勾选 / 半选 / 当前选中状态
const treeRef = ref();
// 仅更新 label,保留子树
treeRef.value.replace({ id: 1, label: "新名称" }, 1);
// 替换子树
treeRef.value.replace({ id: 1, label: "新名称", children: [{ id: 11, label: "子节点" }] }, 1);
Slots
| 插槽名 | 说明 | 参数 |
|---|---|---|
| default | 自定义节点内容 | { node, data } |
| empty | 空状态内容 | - |
| tree-loading | 整个树的加载状态 | - |
| loading | 节点懒加载时的状态 | { node, data } |
| icon | 自定义节点图标 | { node, data } |
大数据量与性能
组件针对百万级节点做了专项优化:
- 分片初始化:扁平化按 8ms 时间片分批执行并让出主线程,初始化期间页面保持可交互(会短暂显示加载状态),百万节点不卡顿
- O(n) 勾选状态:父子关联勾选基于扁平数组一次倒序遍历完成,无 O(n²) 的元数据预计算
- 两种滚动模式(统一由自研
@wxwzl/vue-virtual-list承载,Fenwick 树高度模型,展开/收起仅 O(log n)):- 默认动态行高:支持超长 label 自动换行,行高由 ResizeObserver 实测回写
fixed-height固定行高:O(1) 滚动定位、无高度测量,超大数据量下快速拖拽滚动条不白屏,配合item-size指定行高
- 局部更新:优先使用
append/remove/insertBefore/insertAfter/replace/updateKeyChildren方法做运行时数据变更,避免整树重建
注意:
dataprop 采用浅监听(数组引用变化才触发重建)。原地修改数组或节点字段不会刷新视图,请改用上述命令式方法,或整体替换data数组。
Vapor 模式(实验性,Vue ≥ 3.6)
组件库提供 opt-in 的 Vapor 子路径入口:整链(VirtualTree / VirtualList / TreeNodeItem / TreeNode)按 Vapor 编译,VirtualList 已内联进产物(不依赖 @wxwzl/vue-virtual-list 的 vdom 版本),经 vaporInteropPlugin 与 vdom 应用混跑(仅应用根一处模式边界)。默认入口不受影响,永远是稳定的 vdom 路径。
已知边界:VirtualList 的
rowMemo与dynamic隐藏测量通道是手写的 vdom 子组件,开启后经 interop 混跑(不影响正确性)。
import { createApp, vaporInteropPlugin } from "vue";
import { VirtualTree } from "@wxwzl/vue-virtual-tree/vapor";
import "@wxwzl/vue-virtual-tree/style";
const app = createApp(App);
app.use(vaporInteropPlugin); // 混合模式(vdom 父 → vapor 子)必需
app.mount("#app");
使用前提(均为 Vue 3.6 RC 的打包限制):
-
vue版本 ≥ 3.6; -
应用内必须统一把
vue解析到带 vapor 运行时的独立产物——vapor runtime 与vaporInteropPlugin只存在于vue.runtime-with-vapor.*.js,默认 bundler 入口不含。Vite 配置示例:export default defineConfig({ resolve: { alias: { vue: "vue/dist/vue.runtime-with-vapor.esm-browser.js" }, }, });不可混用两份 vue 产物(会出现静默的响应式系统分裂)。
Vapor 编译基于 Vue 3.6 RC,属实验特性;性能对比见 playground 的「Vapor vs Vdom 基准对比」页。实测在虚拟滚动场景下(可视区仅数十行,实例开销已被虚拟化消除)vapor 暂无性能收益,该入口定位为技术验证与未来就绪,不建议以性能为目的接入。
开发
# 安装依赖
pnpm install
# 启动开发环境
pnpm dev
# 构建
pnpm build
技术栈
- Vue 3 (Composition API)
- TypeScript
- @wxwzl/vue-virtual-list(自研虚拟列表)
- Vite
- pnpm
许可证
MIT
Related Packages
Virtual list, table, grid and tree for Vue 3 — dynamic row heights, grouping, st...
A vue component for tree structure. Support adding treenode/leafnode, editing no...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.4.0 | ^3.4.0 | MIT | |
| 0.3.1 | 3.4.0 | MIT | |
| 0.2.1 | 3.4.0 | MIT | |
| 0.1.14 | 3.4.0 | MIT | |
| 0.1.13 | 3.4.0 | MIT | |
| 0.1.12 | 3.4.0 | MIT | |
| 0.1.11 | 3.4.0 | MIT | |
| 0.1.10 | 3.4.0 | MIT | |
| 0.1.9 | 3.4.0 | MIT | |
| 0.1.8 | 3.4.0 | MIT | |
| 0.1.7 | 3.4.0 | MIT | |
| 0.1.6 | 3.4.0 | MIT | |
| 0.1.5 | 3.4.0 | MIT | |
| 0.1.4 | ^3.3.0 | MIT | |
| 0.1.3 | ^3.3.0 | MIT | |
| 0.1.2 | ^3.3.0 | MIT | |
| 0.1.1 | ^3.3.0 | MIT | |
| 0.1.0 | ^3.3.0 | MIT |