Vue - 在皮尼亚存储一棵树



我要问的问题是关于Pinia的,但实际上可以推广到任何底层商店。

我有一个 Vue + Pinia 应用程序,我希望能够在其中存储一棵树。树由类型Node的对象组成。我需要一次只存储一棵树,我不关心根(我们可以想象它在那里,但我关心的是根的孩子,他们的孩子,等等)。

我想支持以下操作:

  • 创建新的顶级节点
  • 创建作为另一个节点的子节点的节点
  • 修改或删除节点,无论它是顶级节点还是子节点或子节点
  • 我希望能够在节点或整个子树中移动,更改其父级,甚至仅更改其相对于其兄弟姐妹的位置
  • 整个树不会立即从后端获取;它会被懒惰地获取。您可以想象,当在 UI 中打开节点时,会获取其直接子节点,依此类推。

这是我想做的事情:

  • 在我的商店中保留一个包含顶级节点的Node数组。我们称之为topLevelNodes
  • 保留一个对象nodeIdToChildren,它将节点的 ID 映射到作为其子节点的Node数组

我最初会获取顶级节点,填充数组topLevelNodes

对于每个需要知道其子节点的节点,获取它们并将它们放在父 id 下的nodeIdToChildren中作为键。

这种方法的一个优点是可以轻松添加、删除和移动节点:只需触摸映射中的相关条目即可。最大的缺点是,无论节点的位置如何,只找到一个节点的效率都要低得多。假设我想编辑 idxyz的节点,不知道它是谁的孩子。

我可以创建一个 getter,将映射对象中的所有值与顶级节点数组中的值一起展平化,但我不确定其效率。

有没有更好的方法可以做到这一点?

最有效的方法是将所有节点存储在单个数组中,并且在节点的子/父数组中,您只放置其他节点的 ID。

  1. 您应该选择是将子项还是父项存储到项目中,但不能同时存储两者。原则是:你不想将相同的信息存储在两个地方,因为如果它不同步,你就会有微妙/奇怪的错误。例如,如果您存储 id 的子数组:您可以有一个计算parents,但它返回当前在其子数组中包含当前项目 id 的所有项目的 id。同样,如果您存储父项,则子项将是计算的。
  2. 这种平面数组结构允许构建两种类型的 UI 列表:自上而下和自下而上(您可以在两个单独的选项卡中向用户提供这两种列表)。
  3. 一个好的树木导航系统是面包屑(很像计算机上的文件夹)。痕迹导航逻辑相对简单:导航到项目时,将其 id 传递给痕迹导航。如果它已经在痕迹导航中,则它是一个向上导航,您将痕迹导航拼接到该项目。如果不是,则为向下导航,在痕迹导航的末尾添加项目的 id。
  4. 它还允许您轻松地在项目的所有祖先(无论多少级)或项目的所有后代(无论多少级)中搜索某些内容。注意:如果您允许循环关系(A> B> A),则必须在搜索中考虑这一点。

注释

  • 如果要延迟加载,则必须在后端移动搜索和筛选。如果节点总数为数千个,则不应延迟加载。您只请求所有节点一次,然后您可以浏览和搜索它们而无需发出其他请求。它不会影响性能。影响性能的是渲染的节点数多于视口上可以显示的节点数,但这是一个完全不同的主题。这里要说明的另一点是:如果节点有繁重的依赖关系(例如:图像),则仅延迟加载这些依赖关系(例如:在实际显示项目时加载这些依赖项,发出单独的请求(例如:getItemDetails))
  • 以上允许拥有两种类型的系统:允许循环的系统(A> B> A)或不允许循环的系统。每个都有自己的限制类型(递归计算父母/子项时,第一个需要限制);第二个限制从子选择器中排除祖先,并从父选择器中排除后代(假设您构建 UI 来更改子项/父项)。
  • 一个
  • 很大的优势是两个节点之间的子/父关系只存储在一个地方。(例如:如果要将子项从一个父级移动到另一个父级,则必须:从当前父级的childrenID 数组中删除其 ID,将其 ID 添加到新父级的children中。子项本身不受影响,但计算更改parents的值除外。如果要存储parents, - 并且计算children- 则需要更改子数组的parents数组。当然,当您执行此更改时,计算的每个新旧父项children都会更改)。
  • 另一个优点是它允许最大程度的灵活性(任何节点可以同时是任何其他数量节点的子节点或父节点 - 这并不意味着它们必须这样做:您始终可以将父节点的数量限制为一个,并且您将获得"经典">文件夹结构,其中任何节点只能有一个直接父节点)。

您可以在此处查看工作演示。对不起,样式,我只是从不同的沙盒中调整了一些东西。但是您可以获得<TreeView /><TableView />的基本实现。这个有 1k 个节点,但最多 5k 个节点应该没有问题。除此之外,您需要注意渲染的内容和时间。

这没有指定给 Vue/Pinia,所以我只是写了大致的想法,你可以选择实现它的方式。

您将在平面地图上存储树。

const tree = new Map()

每个项目都将是一个node,关键是nodeId。每个节点将包含以下属性:

{
id: "the node id",
parentId: "id of the parent, null if it is the root node",
childIds: "array of the id of its child",
content: "content of the node, whatever you want"
}

让我们来看看您想要的每个运算符:

  • 创建新的顶级节点:
// difficulty: easy
const rootNode = {
id: "nodeId"
parentId: null,
childIds: [...],
content: "..."
}
tree.set(nodeId, rootNode)
  • 创建作为另一个节点的子节点的节点
// difficulty: easy
// add the child first
const childNode = {
id: "nodeId"
parentId: "parentId",
childIds: [...],
content: "..."
}
tree.set(nodeId, childNode)
// add the child id to the parent node
const parentNode = tree.get(parentId)
parentNode.childIds.push(childNode.id)
// set the parent back to your tree
tree.set(parentNode.id, parentNode)
  • 修改或删除节点,无论它是顶级节点还是子节点或子节点
// modify a node. 
// difficulty: easy
const node = tree.get(nodeId)
// ... make the modification
// set it back to the tree
tree.set(nodeId, node)
// delete a node. 
// difficulty: medium
// retrieve the node first
const node = tree.get(nodeId)
// delete it
tree.delete(nodeId)
// delete all of its children
// you need a recursive delete function here to go through all of the node child and child of child and so on
tree.childIds.forEach(recursiveDelete)
// don't forget to delete the nodeId from its parent node. It's easy
...
  • 在节点或整个子树周围移动,更改其父级,甚至仅更改其相对于其同级的位置
// moving around the tree is quite easy, you just need to follow the `parentId` and `childIds`
// changing a node's parent (same level)
// difficulty: easy
// you just need to change the parentId of the node. And modify the childIds of its old and new parent
// changing a node level, moving its children accordingly
// difficulty: easy
// same as changing a node parent above. Its children will move accordingly
// changing a node level to be a child of one of its children
// difficulty: hard
// get the node
const node = tree.get(nodeId)
// go through its children and update the parentId of each to the node.parentId (moving its children to be the direct child of its parent)
node.childIds.forEach((childId)=> updateParentId(childId, node.parentId))
// set the node parentId to the new one
node.parentId = newParentId
// set new childIds for the node if you want
node.childIds = [...]
// don't forget to set it back on the tree
tree.set(node.id, node)
  • 懒洋洋地取树:
// There is no problem at all. You just need to load from the root

优点和缺点

优点:

  • 易于实施
  • 易于获取、更新内容和删除节点,只需通过其 id 即可
  • 轻松将节点及其子节点移动到树中的任何位置

缺点

  • 难以确定节点的级别(您需要遍历其所有父节点)
  • 难以维护数据的约束。假设您找不到节点的父节点
  • 很难准确判断一个节点是否是另一个节点的子节点(子节点的子节点...)(您需要遍历其所有父节点)

最新更新