Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: tree 组件改进列表渲染逻辑 #2586

Merged
merged 10 commits into from
Jul 23, 2023
Merged
2 changes: 1 addition & 1 deletion src/_common
2 changes: 0 additions & 2 deletions src/tree/__tests__/debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ tree 针对性测试命令:
```bash
# 执行单测
npx vitest ./src/tree/__tests__/
# 更新单测快照
npx vitest --updateSnapshot ./src/tree/__tests__/
```

## 调试界面
Expand Down
6 changes: 6 additions & 0 deletions src/tree/_example/activable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,18 @@ export default {
methods: {
onClick(context) {
console.info('onClick', context);
const { node } = context;
console.info(node.value, 'actived:', node.actived);
},
onActive(value, context) {
console.info('onActive', value, context);
const { node } = context;
console.info(node.value, 'actived:', node.actived);
},
propOnActive(value, context) {
console.info('propOnActive', value, context);
const { node } = context;
console.info(node.value, 'actived:', node.actived);
},
toggleActivable() {
this.activable = !this.activable;
Expand Down
6 changes: 6 additions & 0 deletions src/tree/_example/checkable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,18 @@ export default {
methods: {
onClick(context) {
console.info('onClick:', context);
const { node } = context;
console.info(node.value, 'checked:', node.checked);
},
onChange(checked, context) {
console.info('onChange:', checked, context);
const { node } = context;
console.info(node.value, 'checked:', node.checked);
},
propOnChange(checked, context) {
console.info('propOnChange:', checked, context);
const { node } = context;
console.info(node.value, 'checked:', node.checked);
},
selectInvert() {
const { tree } = this.$refs;
Expand Down
10 changes: 10 additions & 0 deletions src/tree/_example/controlled.vue
Original file line number Diff line number Diff line change
Expand Up @@ -169,24 +169,34 @@ export default {
methods: {
onClick(context) {
console.info('onClick:', context);
const { node } = context;
console.info(node.value, 'checked:', node.checked);
console.info(node.value, 'expanded:', node.expanded);
console.info(node.value, 'actived:', node.actived);
},
onChange(vals, context) {
console.info('onChange:', vals, context);
const checked = vals.filter((val) => val !== '2.1');
console.info('节点 2.1 不允许选中');
this.checked = checked;
const { node } = context;
console.info(node.value, 'checked:', node.checked);
},
onExpand(vals, context) {
console.info('onExpand:', vals, context);
const expanded = vals.filter((val) => val !== '2');
console.info('节点 2 不允许展开');
this.expanded = expanded;
const { node } = context;
console.info(node.value, 'expanded:', node.expanded);
},
onActive(vals, context) {
console.info('onActive:', vals, context);
const actived = vals.filter((val) => val !== '2');
console.info('节点 2 不允许激活');
this.actived = actived;
const { node } = context;
console.info(node.value, 'actived:', node.actived);
},
},
};
Expand Down
45 changes: 25 additions & 20 deletions src/tree/_example/debug-vscroll.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
<t-form-item>
<t-button @click="append()">插入根节点</t-button>
</t-form-item>
<t-form-item label="">
<t-input-adornment prepend="filter:">
<t-input v-model="filterText" @change="onInput" />
</t-input-adornment>
</t-form-item>
</t-form>
</t-space>
<t-tree
Expand All @@ -42,6 +47,7 @@
:line="showLine"
:icon="showIcon"
:label="label"
:filter="filterByText"
:scroll="{
rowHeight: 34,
bufferSize: 10,
Expand All @@ -62,7 +68,7 @@
</template>

<script>
const allLevels = [5, 5, 5];
const allLevels = [10, 20, 25];

function createTreeData() {
let cacheIndex = 0;
Expand Down Expand Up @@ -114,27 +120,10 @@ export default {
isCheckable: true,
isOperateAble: true,
items: virtualTree.items,
filterText: '',
filterByText: null,
};
},
computed: {
scroll() {
const { scrollMode } = this;
if (scrollMode === 'normal') {
return null;
}
const scrollProps = {
rowHeight: 34,
bufferSize: 10,
threshold: 10,
};
if (scrollMode === 'lazy') {
scrollProps.type = 'lazy';
} else {
scrollProps.type = 'virtual';
}
return scrollProps;
},
},
methods: {
label(createElement, node) {
return `${node.value}`;
Expand Down Expand Up @@ -162,6 +151,22 @@ export default {
remove(node) {
node.remove();
},
onInput(state) {
console.info('onInput:', state);
if (this.filterText) {
// 存在过滤文案,才启用过滤
this.filterByText = (node) => {
const rs = node.value.indexOf(this.filterText) >= 0;
// 命中的节点会强制展示
// 命中节点的路径节点会锁定展示
// 未命中的节点会隐藏
return rs;
};
} else {
// 过滤文案为空,则还原 tree 为无过滤状态
this.filterByText = null;
}
},
},
};
</script>
91 changes: 39 additions & 52 deletions src/tree/hooks/useTreeNodes.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CreateElement } from 'vue';
import { ref, nextTick, SetupContext } from '@vue/composition-api';
import { ref, SetupContext } from '@vue/composition-api';
import {
TypeVNode, TypeTreeRow, TypeTreeNode, TreeProps, TypeTreeState,
} from '../interface';
Expand All @@ -15,12 +15,48 @@ export default function useTreeNodes(props: TreeProps, context: SetupContext, st
} = treeState;

const { handleClick, handleChange } = useTreeEvents(props, context, state);
const nodesEmpty = ref(false);
// 用于存储已呈现节点的缓存
const cacheMap = new Map();

const refresh = () => {
let list: TypeTreeNode[] = [];
const allNodes = store.getNodes();
const isVirtual = virtualConfig?.isVirtualScroll.value;
if (isVirtual) {
// 虚拟滚动只渲染可见节点
list = virtualConfig.visibleData.value;
nodesEmpty.value = list.length <= 0;
} else {
// 非虚拟滚动,缓存曾经展示过的节点
let hasVisibleNode = false;
allNodes.forEach((node: TypeTreeNode) => {
if (node.visible) {
// 曾经展示过的节点加入缓存,避免再次创建
hasVisibleNode = true;
cacheMap.set(node.value, node.value);
}
if (cacheMap.get(node.value)) {
// 创建的节点是缓存的节点
list.push(node);
}
});
nodesEmpty.value = !hasVisibleNode;
cacheMap.forEach((value) => {
// 在缓存中清理结构变化后不存在的节点
if (!store.getNode(value)) {
cacheMap.delete(value);
}
});
}
// 渲染为平铺列表
nodes.value = list;
};

// 创建单个 tree 节点
const renderItem = (h: CreateElement, node: TypeTreeRow, index: number) => {
const { expandOnClickNode } = props;
const rowIndex = node.__VIRTUAL_SCROLL_INDEX || index;

const treeItem = (
<TreeItem
key={node[privateKey]}
Expand All @@ -35,57 +71,8 @@ export default function useTreeNodes(props: TreeProps, context: SetupContext, st
return treeItem;
};

const cacheMap = new Map();

const nodesEmpty = ref(false);
const refresh = () => {
// 渲染为平铺列表
nodes.value = store.getNodes();
};

const renderTreeNodes = (h: CreateElement) => {
let treeNodeViews: TypeVNode[] = [];
let isEmpty = true;
let list = nodes.value;

const isVirtual = virtualConfig?.isVirtualScroll.value;
if (isVirtual) {
list = virtualConfig.visibleData.value;
nodesEmpty.value = list.length <= 0;
// 虚拟滚动只渲染可见节点
treeNodeViews = list.map((node: TypeTreeNode, index) => renderItem(h, node, index));
} else {
treeNodeViews = list.map((node: TypeTreeNode, index) => {
const nodePrivateKey = node[privateKey];
// 如果节点已经存在,则使用缓存节点
// 不可见的节点,缓存中存在,则依然会保留
let nodeView: TypeVNode = cacheMap.get(nodePrivateKey);
if (node.visible) {
// 任意一个节点可视,过滤结果就不是空
isEmpty = false;
// 如果节点未曾创建,则临时创建
if (!nodeView) {
// 初次仅渲染可显示的节点
// 不存在节点视图,则创建该节点视图并插入到当前位置
nodeView = renderItem(h, node, index);
cacheMap.set(nodePrivateKey, nodeView);
}
}
return nodeView;
});
nodesEmpty.value = isEmpty;

// 更新缓存后,被删除的节点要移除掉,避免内存泄露
nextTick(() => {
cacheMap.forEach((view: TypeVNode, nodePrivateKey: string) => {
const node = store.privateMap.get(nodePrivateKey);
if (!node) {
cacheMap.delete(nodePrivateKey);
}
});
});
}

const treeNodeViews: TypeVNode[] = nodes.value.map((node: TypeTreeNode, index) => renderItem(h, node, index));
return treeNodeViews;
};

Expand Down
5 changes: 3 additions & 2 deletions src/tree/hooks/useTreeScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { TreeProps, TypeTreeState, TypeTimer } from '../interface';
export default function useTreeScroll(props: TreeProps, context: SetupContext, state: TypeTreeState) {
const treeState = state;
const {
scope, treeContentRef, nodes, isScrolling,
allNodes, nodes, scope, treeContentRef, isScrolling,
} = treeState;

const scrollProps: Ref<TScroll> = computed(() => ({
Expand All @@ -22,7 +22,7 @@ export default function useTreeScroll(props: TreeProps, context: SetupContext, s

// 虚拟滚动
const virtualScrollParams = computed(() => {
const list = nodes.value.filter((node: TreeNode) => node.visible);
const list = allNodes.value.filter((node: TreeNode) => node.visible);
return {
data: list,
scroll: scrollProps.value,
Expand Down Expand Up @@ -70,6 +70,7 @@ export default function useTreeScroll(props: TreeProps, context: SetupContext, s
if (lastScrollY !== top) {
if (isVirtual) {
virtualConfig.handleScroll();
nodes.value = virtualConfig.visibleData.value;
}
} else {
lastScrollY = 0;
Expand Down
5 changes: 5 additions & 0 deletions src/tree/hooks/useTreeState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ import TreeNode from '../../_common/js/tree/tree-node';
export default function useTreeState(props: TreeProps, store: TypeTreeStore) {
const treeContentRef = ref<HTMLDivElement>();
const nodes: Ref<TreeNode[]> = ref([]);
const allNodes: Ref<TreeNode[]> = ref([]);
const isScrolling: Ref<boolean> = ref(false);

allNodes.value = store.getNodes();

const state: TypeTreeState = {
// tree 数据对象
store,
// 内容根节点
treeContentRef,
// 渲染节点
nodes,
// 所有节点
allNodes,
// 是否正在滚动
isScrolling,
// 缓存点击事件
Expand Down
3 changes: 2 additions & 1 deletion src/tree/hooks/useTreeStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export default function useTreeStore(props: TreeProps, context: SetupContext) {
// 所以在 update 之后检查,如果之前 filter 有变更,则检查路径节点是否需要展开
// 如果 filter 属性被清空,则重置为开启搜索之前的结果
const expandFilterPath = () => {
if (!props.allowFoldNodeOnFilter) return;
if (!filterChanged) return;
// 确保 filter 属性未变更时,不会重复检查展开状态
filterChanged = false;
Expand Down Expand Up @@ -102,7 +103,7 @@ export default function useTreeStore(props: TreeProps, context: SetupContext) {
};

// 这个方法监听 filter 属性,仅在 allowFoldNodeOnFilter 属性为 true 时生效
// 仅在 filter 属性发生变更时开启检查开关,避免其他操作也触发展开状态的充值
// 仅在 filter 属性发生变更时开启检查开关,避免其他操作也触发展开状态的重置
const checkFilterExpand = (newFilter: null | Function, previousFilter: null | Function) => {
if (!props.allowFoldNodeOnFilter) return;
filterChanged = newFilter !== previousFilter;
Expand Down
1 change: 1 addition & 0 deletions src/tree/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface TypeTreeState {
scope: TypeTreeScope;
store: TypeTreeStore;
nodes: Ref<TreeNode[]>;
allNodes: Ref<TreeNode[]>;
isScrolling: Ref<boolean>;
treeContentRef: Ref<HTMLDivElement>;
mouseEvent?: Event;
Expand Down
Loading
Loading