简体   繁体   English

Vue - 在 Pinia 中存储一棵树

[英]Vue - Storing a tree in Pinia

The question I'm going to ask is about Pinia, but really could just be generalized to any underlying store.我要问的问题是关于 Pinia 的,但实际上可以推广到任何基础商店。

I have a Vue + Pinia application in which I want to be able to store a tree.我有一个 Vue + Pinia 应用程序,我希望能够在其中存储一棵树。 The tree is made up of objects of type Node .该树由Node类型的对象组成。 I need to store exactly one tree at a time, and I don't care about the root (we can imagine it's there, but what I care about is the root's children, their children, and so on).我需要一次只存储一棵树,我不关心根(我们可以想象它在那里,但我关心的是根的孩子,他们的孩子,等等)。

I want to support the following operations:我想支持以下操作:

  • create a new top-level node创建一个新的顶级节点
  • create a node that's child of another node创建一个节点,该节点是另一个节点的子节点
  • modify or delete a node, regardless of whether it's top-level or child or child of a child修改或删除节点,无论它是顶级节点还是子节点或子节点的子节点
  • I want to be able to move around a node or an entire subtree, changing its parent or even just its position relative to its siblings我希望能够围绕一个节点或整个子树移动,改变它的父节点,甚至只是它的 position 相对于它的兄弟节点
  • the whole tree won't be fetched from the backend at once;不会立即从后端获取整棵树; it'll be fetched lazily.它会被懒惰地获取。 You can imagine when a node is opened in the UI, its direct children are fetched and so on.您可以想象当一个节点在 UI 中打开时,它的直接子节点被获取等等。

Here's something I have thought of doing:这是我想做的事情:

  • keep in my store an array of Node s containing the top level nodes.在我的商店中保留一个包含顶级节点的Node数组。 Let's call it topLevelNodes我们称它为topLevelNodes
  • keep an object nodeIdToChildren , which maps the id of a node to an array of Node s that are its children保留一个 object nodeIdToChildren ,它将节点的 id 映射到作为其子节点的Node数组

I would initially fetch the top level nodes, filling the array topLevelNodes .我最初会获取顶级节点,填充数组topLevelNodes

For each node that needs to know its children, fetch them and put them in nodeIdToChildren under the parent id as key.对于每个需要知道其子节点的节点,获取它们并将它们作为键放入父 ID 下的nodeIdToChildren中。

An advantage of this approach is that it's easy to add, delete, and move around nodes: just touch the relevant entries in the mapping.这种方法的一个优点是很容易添加、删除和移动节点:只需触摸映射中的相关条目。 The biggest drawback is that it's much less efficient to just find a node, regardless of its position. Say I want to edit node with id xyz , not knowing whose child it is.最大的缺点是只找到一个节点的效率要低得多,不管它的 position。假设我想编辑 id 为xyz的节点,不知道它是谁的孩子。

I could create a getter that flattens all the values in the mapping object together with the values in the top level nodes array, but I'm not sure about efficiency of that.我可以创建一个吸气剂,将映射 object 中的所有值与顶级节点数组中的值一起展平,但我不确定其效率。

Are there any better ways of doing this?有没有更好的方法来做到这一点?

The most efficient way is to store all nodes in a single array and in the children/parents array of a node you only place ids of other nodes.最有效的方法是将所有节点存储在一个数组中,而在一个节点的子/父数组中,您只放置其他节点的 ID。

  1. You should choose whether you store children or parents into an item, but not both.您应该选择是将孩子还是父母存储到一个项目中,而不是同时存储两者。 The principle is: you don't want to store the same information in two places, because if it goes out of sync you'll have subtle/weird bugs.原则是:你不想在两个地方存储相同的信息,因为如果不同步,你就会有微妙/奇怪的错误。 For example, if you store children array of ids: you can have a computed parents , but it returns the ids of all items which currently contain the current item's id in their children array.例如,如果你存储 children 的 ids 数组:你可以有一个计算的parents ,但它返回所有项目的 ids,这些项目当前在他们的 children 数组中包含当前项目的 id。 Similarly, if you store parents, the children will be a computed.同样,如果您存储父母,则孩子将被计算。
  2. This flat array structure allows building two type of UI lists: top-down and bottom-up (you can provide both to the user, in two separate tabs).这种平面数组结构允许构建两种类型的 UI 列表:自上而下和自下而上(您可以在两个单独的选项卡中向用户提供这两种列表)。
  3. A good navigation system for trees is breadcrumb (pretty much like folders on a computer).一个很好的树导航系统是面包屑(很像计算机上的文件夹)。 The breadcrumb logic is relatively simple: When you navigate to an item, you pass its id to the breadcrumb.面包屑逻辑相对简单:当您导航到某个项目时,您将其 id 传递给面包屑。 If it's already in the breadcrumb, it's an upwards navigation and you splice the breadcrumb to that item.如果它已经在面包屑中,则为向上导航,您将面包屑拼接到该项目。 If it's not, it's a downwards navigation, you add the item's id at the end of the breadcrumb.如果不是,则为向下导航,您在面包屑末尾添加项目的 id。
  4. It also allows you to easily search for something in all ancestors of an item (no matter how many levels) or all descendants of an item (no matter how many levels).它还允许您轻松地在项目的所有祖先(无论多少级)或项目的所有后代(无论多少级)中搜索某些内容。 Note: if you allow cyclic relations (A > B > A), you have to account for that in your searches.注意:如果您允许循环关系 (A > B > A),则必须在搜索中考虑到这一点。

Notes :注意事项

  • If you want to lazy load, you'll have to move search and filtering on backend.如果您想延迟加载,则必须在后端移动搜索和过滤。 If the total number of nodes is in the thousands, you should not lazy-load.如果节点总数以千计,则不应延迟加载。 You only request all nodes once, and then you can navigate & search through them without making another request.您只需请求所有节点一次,然后您可以在不发出另一个请求的情况下导航和搜索它们。 It won't affect performance.它不会影响性能。 What does affect performance is rendering more nodes than you can display on a viewport, but that's a completely different subject.影响性能的是渲染的节点多于您可以在视口上显示的节点,但这是一个完全不同的主题。 Another point to be made here: if nodes have heavy dependencies (eg: images), only lazy load those dependencies (eg: load those when you actually display the item, making a separate request (eg: getItemDetails ))此处要说明的另一点:如果节点具有重依赖性(例如:图像),则仅延迟加载这些依赖项(例如:在实际显示项目时加载那些,发出单独的请求(例如: getItemDetails ))
  • The above allows having both types of systems: the ones which allow cycles (A > B > A) or which don't allow.以上允许拥有两种类型的系统:允许循环的系统(A > B > A)或不允许的系统。 Each one has its own type of limitations (the first one needs limitation when recursively calculating parents/children);每个都有自己的限制类型(第一个在递归计算父母/孩子时需要限制); the second one has limitation to exclude ancestors from children selector and to exclude descendants from parents selector (assuming you build UI to change children/parents).第二个限制从子选择器中排除祖先并从父选择器中排除后代(假设您构建 UI 以更改子/父母)。
  • a big advantage is that child/parent relation between two nodes is only store in one place.一个很大的优势是两个节点之间的子/父关系只存储在一个地方。 (example: if you want to move a child from one parent to another you have to: remove its ID from the current parent's children array of ids, add its ID to the new parent's children . The child itself is not affected, except the value of its parents computed changes. If you're storing parents , - and children is computed - you need to change the child's parents array. And, of course, each of the old and new parent's children computed will change when you perform this change). (例如:如果你想将一个孩子从一个父母移动到另一个父母,你必须:从当前父母的children ID 数组中删除它的 ID,将它的 ID 添加到新父母的children中。孩子本身不受影响,除了值它的parents计算的变化。如果你正在存储parents , - 并且计算了children - 你需要改变孩子的parents数组。当然,当你执行这个改变时,每个新旧父母的children计算都会改变) .
  • another advantage is it allows a maximum level of flexibility (any node can be child or parent of any other number of nodes simultaneously - which doesn't mean they have to: you can always limit the number of parents to one and you get the "classic" folder structure where any node can only have one direct parent).另一个优点是它允许最大程度的灵活性(任何节点都可以同时是任何其他数量的节点的子节点或父节点——这并不意味着它们必须:你总是可以将父节点的数量限制为一个,并且你得到“经典”文件夹结构,其中任何节点只能有一个直接父节点)。

You can see a working demo here .您可以在此处查看工作演示 Sorry about the styles, I just tweaked something from a different sandbox.对不起 styles,我只是从不同的沙盒中调整了一些东西。 But you get the basic implementations of both <TreeView /> and <TableView /> .但是你得到了<TreeView /><TableView />的基本实现。 This one has 1k nodes but should have no issues up to 5k nodes.这个有 1k 个节点,但最多 5k 个节点应该没有问题。 Above that, you need to be careful what you render and when.除此之外,您需要注意渲染的内容和时间。

This is not specified to Vue/Pinia so I just write the general idea, you can choose the way to implement it.这不是Vue/Pinia特有的,所以我只是写了大概的思路,你可以选择实现的方式。

You will store your tree on a flat map.您将把您的树存放在一个平面 map 上。

const tree = new Map()

Each item will be a node and the key is the nodeId .每个项目都是一个node ,键是nodeId Each node will contain these properties:每个节点将包含以下属性:

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

Let's go through each operator you want:让我们通过您想要的每个运营商 go:

  • create a new top-level node:创建一个新的顶级节点:
// difficulty: easy
const rootNode = {
  id: "nodeId"
  parentId: null,
  childIds: [...],
  content: "..."
}

tree.set(nodeId, rootNode)
  • create a node that's a child of another node创建一个节点,该节点是另一个节点的子节点
// 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 or delete a node, regardless of whether it's top-level or child or child of a child修改或删除节点,无论它是顶级节点还是子节点或子节点的子节点
// 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
...
  • move around a node or an entire subtree, changing its parent or even just its position relative to its siblings围绕一个节点或整个子树移动,改变它的父节点,甚至只是它的 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)
  • fetch the tree lazily:懒惰地获取树:
// There is no problem at all. You just need to load from the root

Pros and Cons优点和缺点

Pros:优点:

  • Easy implement易于实施
  • Easy to get, update content, and delete a node just by its id仅通过其 id 即可轻松获取、更新内容和删除节点
  • Easy to move a node along with its children to anywhere you want in the tree轻松将节点及其子节点移动到树中您想要的任何位置

Cons缺点

  • Hard to determine the level of a node (You need to loop through all of its parents)难以确定节点的级别(您需要遍历其所有父节点)
  • Hard to maintain the constraints of data.难以维护数据的约束。 Let's say you can't find the parent of a node假设您找不到节点的父节点
  • Hard to tell exactly if a node is a child (child of child...) of another node (You need to loop through all of its parents)很难准确判断一个节点是否是另一个节点的子节点(子节点的子节点...)(您需要遍历其所有父节点)

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

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