繁体   English   中英

Vue - 在 Pinia 中存储一棵树

[英]Vue - Storing a tree in Pinia

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

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

我想支持以下操作:

  • 创建一个新的顶级节点
  • 创建一个节点,该节点是另一个节点的子节点
  • 修改或删除节点,无论它是顶级节点还是子节点或子节点的子节点
  • 我希望能够围绕一个节点或整个子树移动,改变它的父节点,甚至只是它的 position 相对于它的兄弟节点
  • 不会立即从后端获取整棵树; 它会被懒惰地获取。 您可以想象当一个节点在 UI 中打开时,它的直接子节点被获取等等。

这是我想做的事情:

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

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

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

这种方法的一个优点是很容易添加、删除和移动节点:只需触摸映射中的相关条目。 最大的缺点是只找到一个节点的效率要低得多,不管它的 position。假设我想编辑 id 为xyz的节点,不知道它是谁的孩子。

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

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

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

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

注意事项

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

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

这不是Vue/Pinia特有的,所以我只是写了大概的思路,你可以选择实现的方式。

您将把您的树存放在一个平面 map 上。

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"
}

让我们通过您想要的每个运营商 go:

  • 创建一个新的顶级节点:
// 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
...
  • 围绕一个节点或整个子树移动,改变它的父节点,甚至只是它的 position 相对于它的兄弟节点
// 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 即可轻松获取、更新内容和删除节点
  • 轻松将节点及其子节点移动到树中您想要的任何位置

缺点

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM