如何使用递归 JavaScript 映射方法从 parent/child 关系创建新对象
How to create a new object from parent/child relationships using recursive JavaScript map method
我有一组对象。其中一些有一个 wordpress_parent
属性,其值为 `.这意味着该节点是另一个节点的子节点。实际的最终结果是一个嵌套的评论UI,所以可以有多个子级。
我想遍历我的对象,在 wordpress_parent !== 0
的位置找到原始数组中 wordpress_id
等于 wordpress_parent
的值的对象,并将该对象作为匹配父节点的子节点 属性。
我要实现这个对象形式:
node {
...originalPropsHere,
children: { ...originalChildNodeProps }
}
我的想法是创建一个新数组,该数组具有正确的父级、子级嵌套结构,然后我可以对其进行迭代并将其输出到 JSX 结构中。
我想编写一个执行此逻辑的递归函数,然后 returns 一个像这样的 JSX 注释结构(基本上):
<article className="comment" key={node.wordpress_id}>
<header>
<a href={node.author_url} rel="nofollow"><h4>{node.author_name}</h4></a>
<span>{node.date}</span>
</header>
{node.content}
</article>
我想我必须使用 JavaScript 的 map
方法来创建一个新数组。我遇到的问题是操纵数据在我的父节点上创建一个新的 children
属性,然后将匹配的子评论作为 属性 的值。然后将它放在一个漂亮的小函数中,递归地创建我可以在我的组件中呈现的 HTML/JSX 结构。
聪明的人,请站起来,谢谢! :D
这是对 的修改,它处理额外的 node
包装器和您的 ID 以及 parent 属性 名称:
const nest = (xs, id = 0) =>
xs .filter (({node: {wordpress_parent}}) => wordpress_parent == id)
.map (({node: {wordpress_id, wordpress_parent, ...rest}}) => ({
node: {
...rest,
wordpress_id,
children: nest (xs, wordpress_id)
}
}))
const edges = [
{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}},
{node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}},
{node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}},
{node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}},
{node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}},
{node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}},
{node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}
]
console .log (
nest (edges)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
它在那些没有 children 的节点中包含一个空的 children
数组。 (如果你知道你最多只能有一个 child 并且更喜欢这个名字 child
,那么这将需要一些修改;但应该不错。)
基本上,它需要一个项目列表和一个 id 来测试,并过滤具有该 id 的列表。然后它通过使用当前object.
的id递归调用函数来添加一个children
属性
因为 wordpress_parent
包含在传递给 map
的函数的解构参数中但不包含在输出中,因此跳过此节点。如果你想保留它,你可以将它添加到输出中,但更简单的是将它作为参数跳过;那么它将成为 ...rest
.
的一部分
更新:泛化
励志。我已经用相同答案的变体回答了很多这样的问题。泛化为可重用函数已经过去了。
该答案创建了所有值的索引,然后使用该索引构建输出。我上面的技术(以及其他几个答案)有些不同:扫描数组以查找所有根元素,并为每个根元素扫描数组以查找它们的 children,并且对于其中的每一个,扫描数组以查找grandchildren 等。这可能效率较低,但更容易泛化,因为不需要为每个元素生成代表键。
所以我在一个更通用的解决方案中创建了第一遍,其中我上面所做的被分成两个更简单的函数,它们被传递(连同原始数据和根值的表示)到一个将它们放在一起并处理递归的通用函数。
下面是使用此函数解决当前问题的示例:
// forest :: [a] -> (a, (c -> [b]) -> b) -> ((c, a) -> Bool) -> c -> [b]
const forest = (xs, build, isChild, root) =>
xs .filter (x => isChild (root, x))
.map (node => build (node, root => forest (xs, build, isChild, root)))
const edges = [{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}}, {node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}}, {node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}}, {node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}}, {node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}}, {node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}}, {node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}]
const result = forest (
edges,
(x, f) => ({node: {...x.node, children: f (x.node.wordpress_id)}}),
(id, {node: {wordpress_parent}}) => wordpress_parent == id,
0
)
console .log (result)
.as-console-wrapper {min-height: 100% !important; top: 0}
我使用 forest
而不是 tree
,因为这里生成的实际上不是一棵树。 (它有多个根节点。)但它的参数与 Thankyou 的非常相似。其中最复杂的 build
正好等同于该答案的 maker
。 xs
等同于 all
,root
参数(几乎)等同。 Thankyou 的 indexer
和我的 isChild
之间的主要区别。因为 Thankyou 生成元素的外键映射,indexer
需要一个节点和 returns 节点的表示,通常是 属性。我的版本是二元谓词。它需要当前元素和第二个元素的表示,并且 returns true
当且仅当第二个元素是当前元素的 child 时。
root
参数的不同样式
最后一个参数 root
实际上相当有趣。它需要是当前 object 的某种代表。但它不需要是任何特定的代表。在简单的情况下,这可以只是类似于 id
参数的东西。但它也可以是实际元素。这也行得通:
console .log (forest (
edges,
(x, f) => ({node: {...x.node, children: f (x)}}),
(p, c) => p.node.wordpress_id == c.node.wordpress_parent,
{node: {wordpress_id: 0}}
))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))
const edges = [{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}}, {node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}}, {node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}}, {node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}}, {node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}}, {node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}}, {node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}]</script>
在这种情况下,最终参数更复杂,是一个 object 在结构上类似于列表中的典型元素,在这种情况下具有根 ID。但是当我们这样做时,参数 isChild
和 build
提供的回调会更简单一些。要记住的是,这是传递给 isChild
的结构。在第一个例子中只有 id,所以 root
参数很简单,但其他函数有点复杂。在第二个中,root
更复杂,但它允许我们简化其他参数。
其他变换
这可以很容易地应用于其他示例。前面提到的可以这样处理:
const flat = [
{id: "a", name: "Root 1", parentId: null},
{id: "b", name: "Root 2", parentId: null},
{id: "c", name: "Root 3", parentId: null},
{id: "a1", name: "Item 1", parentId: "a"},
{id: "a2", name: "Item 1", parentId: "a"},
{id: "b1", name: "Item 1", parentId: "b"},
{id: "b2", name: "Item 2", parentId: "b"},
{id: "b2-1", name: "Item 2-1", parentId: "b2"},
{id: "b2-2", name: "Item 2-2", parentId: "b2"},
{id: "b3", name: "Item 3", parentId: "b"},
{id: "c1", name: "Item 1", parentId: "c"},
{id: "c2", name: "Item 2", parentId: "c"}
]
console .log (forest (
flat,
({id, parentId, ...rest}, f) => ({id, ...rest, children: f (id)}),
(id, {parentId}) => parentId == id,
null
))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))</script>
或者 可能如下所示:
const input = [
{ forumId: 3, parentId: 1, forumName: "General", forumDescription: "General forum, talk whatever you want here", forumLocked: false, forumDisplay: true },
{ forumId: 2, parentId: 1, forumName: "Announcements", forumDescription: "Announcements & Projects posted here", forumLocked: false, forumDisplay: true },
{ forumId: 4, parentId: 3, forumName: "Introduction", forumDescription: "A warming introduction for newcomers here", forumLocked: false, forumDisplay: true },
{ forumId: 1, parentId: null, forumName: "Main", forumDescription: "", forumLocked: false, forumDisplay: true }
]
console .log (forest (
input,
(node, f) => ({...node, subforum: f(node .forumId)}),
(id, {parentId}) => parentId == id,
null
))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))</script>
输入结构明显不同
这些输入结构都相似,因为每个节点都指向其 parent 的标识符,当然根节点除外。但是这种技术与 parents 指向其 children 的标识符列表的技术一样有效。创建根元素(这里还有一个辅助函数)需要做更多的工作,但同样的系统将允许我们混合这样的模型:
const xs = [
{content: 'abc', wordpress_id: 196, child_ids: []},
{content: 'def', wordpress_id: 193, child_ids: [196, 199]},
{content: 'ghi', wordpress_id: 199, child_ids: []},
{content: 'jkl', wordpress_id: 207, child_ids: [208, 224]},
{content: 'mno', wordpress_id: 208, child_ids: [209]},
{content: 'pqr', wordpress_id: 209, child_ids: []},
{content: 'stu', wordpress_id: 224, child_ids: []}
]
const diff = (xs, ys) => xs .filter (x => !ys.includes(x))
console .log (forest (
xs,
(node, fn) => ({...node, children: fn(node)}),
({child_ids}, {wordpress_id}) => child_ids .includes (wordpress_id),
{child_ids: diff (xs .map (x => x .wordpress_id), xs .flatMap (x => x .child_ids))}
))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))</script>
这里我们有不同风格的isChild
,测试潜在child的id是否在parent提供的id列表中。为了创建初始根,我们必须扫描 id 列表,寻找那些没有显示为child 个 ID。我们使用 diff
帮助程序来执行此操作。
我在上面讨论额外的灵活性时提到了这种不同的风格。
只有第一关
我将此称为此类解决方案的“第一关”,因为这里有些地方我不是很满意。我们可以使用这种技术来处理删除 now-unnecessary parent id,并且如果实际上有实际要包含的 children 节点,也可以只包含一个 children
节点。对于原始示例,它可能如下所示:
console .log (forest (
edges,
( {node: {wordpress_id, wordpress_parent, ...rest}},
f,
kids = f (wordpress_id)
) => ({node: {
...rest,
wordpress_id,
...(kids.length ? {children: kids} : {})
}}),
(id, {node: {wordpress_parent}}) => wordpress_parent == id,
0
))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))
const edges = [{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}}, {node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}}, {node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}}, {node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}}, {node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}}, {node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}}, {node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}]</script>
请注意,如果有内容,结果现在只包含 children
。 wordpress_parent
节点,现在是多余的,已被删除。
所以这可以用这种技术实现,我们可以对其他示例做类似的事情。但它在 build
函数中具有相当高的复杂性。我希望进一步反思可以产生一种简化这两个功能的方法。所以它仍在进行中。
结论
这种概括,将可重用 functions/modules 保存为个人工具包的一部分,可以极大地改进我们的代码库。我们刚刚将上面的相同函数用于许多明显相关但略有不同的行为。那只能是一场胜利。
这不是完整的代码,但它可以像这样使用,并且有多种改进途径可供追求。
非常感谢 shout-out 的启发。我可能早就应该这样做了,但不知何故这次它让我明白了。谢谢!
学习可重用模块和相互递归的好机会。此答案中的解决方案解决了您的特定问题,而无需对 中编写的模块进行任何修改。 @ScottSauyet,感谢您提供具体的 input
示例 -
// Main.js
import { tree } from './Tree' // <- use modules!
const input =
[ { node: { content: 'abc', wordpress_id: 196, wordpress_parent: 193 } }
, { node: { content: 'def', wordpress_id: 193, wordpress_parent: null } } // <- !
, { node: { content: 'ghi', wordpress_id: 199, wordpress_parent: 193 } }
, { node: { content: 'jkl', wordpress_id: 207, wordpress_parent: null } } // <- !
, { node: { content: 'mno', wordpress_id: 208, wordpress_parent: 207 } }
, { node: { content: 'pqr', wordpress_id: 209, wordpress_parent: 208 } }
, { node: { content: 'stu', wordpress_id: 224, wordpress_parent: 207 } }
]
const result =
tree // <- make a tree
( input // <- array of nodes
, ({ node }) => node.wordpress_parent // <- foreign key
, ({ node }, child) => // <- node reconstructor function
({ node: { ...node, child: child(node.wordpress_id) } }) // <- primary key
)
console.log(JSON.stringify(result, null, 2))
输出-
[
{
"node": {
"content": "def",
"wordpress_id": 193,
"wordpress_parent": null,
"child": [
{
"node": {
"content": "abc",
"wordpress_id": 196,
"wordpress_parent": 193,
"child": []
}
},
{
"node": {
"content": "ghi",
"wordpress_id": 199,
"wordpress_parent": 193,
"child": []
}
}
]
}
},
{
"node": {
"content": "jkl",
"wordpress_id": 207,
"wordpress_parent": null,
"child": [
{
"node": {
"content": "mno",
"wordpress_id": 208,
"wordpress_parent": 207,
"child": [
{
"node": {
"content": "pqr",
"wordpress_id": 209,
"wordpress_parent": 208,
"child": []
}
}
]
}
},
{
"node": {
"content": "stu",
"wordpress_id": 224,
"wordpress_parent": 207,
"child": []
}
}
]
}
}
]
在input
中,我用wordpress_parent = null
表示一个根节点。如果需要,我们可以像在您的原始程序中一样使用 0
。 tree
接受第四个参数,root
,select 的节点作为树的基础。默认是 null
但我们可以指定 0
,比如 -
const input =
[ { node: { content: 'abc', wordpress_id: 196, wordpress_parent: 193 } }
, { node: { content: 'def', wordpress_id: 193, wordpress_parent: 0 } } // <- !
, { node: { content: 'ghi', wordpress_id: 199, wordpress_parent: 193 } }
, { node: { content: 'jkl', wordpress_id: 207, wordpress_parent: 0 } } // <- !
, { node: { content: 'mno', wordpress_id: 208, wordpress_parent: 207 } }
, { node: { content: 'pqr', wordpress_id: 209, wordpress_parent: 208 } }
, { node: { content: 'stu', wordpress_id: 224, wordpress_parent: 207 } }
]
const result =
tree
( input
, ({ node }) => node.wordpress_parent
, ({ node }, child) =>
({ node: { ...node, child: child(node.wordpress_id) } })
, 0 // <- !
)
console.log(JSON.stringify(result, null, 2))
// same output ...
为了使这个 post 完整,我将包含 Tree
模块的副本 -
// Tree.js
import { index } from './Index'
const empty =
{}
function tree (all, indexer, maker, root = null)
{ const cache =
index(all, indexer)
const many = (all = []) =>
all.map(x => one(x))
// zero knowledge of node shape
const one = (single) =>
maker(single, next => many(cache.get(next)))
return many(cache.get(root))
}
export { empty, tree } // <- public interface
和Index
模块依赖-
// Index.js
const empty = _ =>
new Map
const update = (r, k, t) =>
r.set(k, t(r.get(k)))
const append = (r, k, v) =>
update(r, k, (all = []) => [...all, v])
const index = (all = [], indexer) =>
all.reduce
( (r, v) => append(r, indexer(v), v) // zero knowledge of value shape
, empty()
)
export { empty, index, append } // <- public interface
如需更多见解,我鼓励您阅读 。
我有一组对象。其中一些有一个 wordpress_parent
属性,其值为 `.这意味着该节点是另一个节点的子节点。实际的最终结果是一个嵌套的评论UI,所以可以有多个子级。
我想遍历我的对象,在 wordpress_parent !== 0
的位置找到原始数组中 wordpress_id
等于 wordpress_parent
的值的对象,并将该对象作为匹配父节点的子节点 属性。
我要实现这个对象形式:
node {
...originalPropsHere,
children: { ...originalChildNodeProps }
}
我的想法是创建一个新数组,该数组具有正确的父级、子级嵌套结构,然后我可以对其进行迭代并将其输出到 JSX 结构中。
我想编写一个执行此逻辑的递归函数,然后 returns 一个像这样的 JSX 注释结构(基本上):
<article className="comment" key={node.wordpress_id}>
<header>
<a href={node.author_url} rel="nofollow"><h4>{node.author_name}</h4></a>
<span>{node.date}</span>
</header>
{node.content}
</article>
我想我必须使用 JavaScript 的 map
方法来创建一个新数组。我遇到的问题是操纵数据在我的父节点上创建一个新的 children
属性,然后将匹配的子评论作为 属性 的值。然后将它放在一个漂亮的小函数中,递归地创建我可以在我的组件中呈现的 HTML/JSX 结构。
聪明的人,请站起来,谢谢! :D
这是对 node
包装器和您的 ID 以及 parent 属性 名称:
const nest = (xs, id = 0) =>
xs .filter (({node: {wordpress_parent}}) => wordpress_parent == id)
.map (({node: {wordpress_id, wordpress_parent, ...rest}}) => ({
node: {
...rest,
wordpress_id,
children: nest (xs, wordpress_id)
}
}))
const edges = [
{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}},
{node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}},
{node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}},
{node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}},
{node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}},
{node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}},
{node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}
]
console .log (
nest (edges)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
它在那些没有 children 的节点中包含一个空的 children
数组。 (如果你知道你最多只能有一个 child 并且更喜欢这个名字 child
,那么这将需要一些修改;但应该不错。)
基本上,它需要一个项目列表和一个 id 来测试,并过滤具有该 id 的列表。然后它通过使用当前object.
的id递归调用函数来添加一个children
属性
因为 wordpress_parent
包含在传递给 map
的函数的解构参数中但不包含在输出中,因此跳过此节点。如果你想保留它,你可以将它添加到输出中,但更简单的是将它作为参数跳过;那么它将成为 ...rest
.
更新:泛化
该答案创建了所有值的索引,然后使用该索引构建输出。我上面的技术(以及其他几个答案)有些不同:扫描数组以查找所有根元素,并为每个根元素扫描数组以查找它们的 children,并且对于其中的每一个,扫描数组以查找grandchildren 等。这可能效率较低,但更容易泛化,因为不需要为每个元素生成代表键。
所以我在一个更通用的解决方案中创建了第一遍,其中我上面所做的被分成两个更简单的函数,它们被传递(连同原始数据和根值的表示)到一个将它们放在一起并处理递归的通用函数。
下面是使用此函数解决当前问题的示例:
// forest :: [a] -> (a, (c -> [b]) -> b) -> ((c, a) -> Bool) -> c -> [b]
const forest = (xs, build, isChild, root) =>
xs .filter (x => isChild (root, x))
.map (node => build (node, root => forest (xs, build, isChild, root)))
const edges = [{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}}, {node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}}, {node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}}, {node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}}, {node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}}, {node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}}, {node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}]
const result = forest (
edges,
(x, f) => ({node: {...x.node, children: f (x.node.wordpress_id)}}),
(id, {node: {wordpress_parent}}) => wordpress_parent == id,
0
)
console .log (result)
.as-console-wrapper {min-height: 100% !important; top: 0}
我使用 forest
而不是 tree
,因为这里生成的实际上不是一棵树。 (它有多个根节点。)但它的参数与 Thankyou 的非常相似。其中最复杂的 build
正好等同于该答案的 maker
。 xs
等同于 all
,root
参数(几乎)等同。 Thankyou 的 indexer
和我的 isChild
之间的主要区别。因为 Thankyou 生成元素的外键映射,indexer
需要一个节点和 returns 节点的表示,通常是 属性。我的版本是二元谓词。它需要当前元素和第二个元素的表示,并且 returns true
当且仅当第二个元素是当前元素的 child 时。
root
参数的不同样式
最后一个参数 root
实际上相当有趣。它需要是当前 object 的某种代表。但它不需要是任何特定的代表。在简单的情况下,这可以只是类似于 id
参数的东西。但它也可以是实际元素。这也行得通:
console .log (forest (
edges,
(x, f) => ({node: {...x.node, children: f (x)}}),
(p, c) => p.node.wordpress_id == c.node.wordpress_parent,
{node: {wordpress_id: 0}}
))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))
const edges = [{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}}, {node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}}, {node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}}, {node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}}, {node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}}, {node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}}, {node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}]</script>
在这种情况下,最终参数更复杂,是一个 object 在结构上类似于列表中的典型元素,在这种情况下具有根 ID。但是当我们这样做时,参数 isChild
和 build
提供的回调会更简单一些。要记住的是,这是传递给 isChild
的结构。在第一个例子中只有 id,所以 root
参数很简单,但其他函数有点复杂。在第二个中,root
更复杂,但它允许我们简化其他参数。
其他变换
这可以很容易地应用于其他示例。前面提到的
const flat = [
{id: "a", name: "Root 1", parentId: null},
{id: "b", name: "Root 2", parentId: null},
{id: "c", name: "Root 3", parentId: null},
{id: "a1", name: "Item 1", parentId: "a"},
{id: "a2", name: "Item 1", parentId: "a"},
{id: "b1", name: "Item 1", parentId: "b"},
{id: "b2", name: "Item 2", parentId: "b"},
{id: "b2-1", name: "Item 2-1", parentId: "b2"},
{id: "b2-2", name: "Item 2-2", parentId: "b2"},
{id: "b3", name: "Item 3", parentId: "b"},
{id: "c1", name: "Item 1", parentId: "c"},
{id: "c2", name: "Item 2", parentId: "c"}
]
console .log (forest (
flat,
({id, parentId, ...rest}, f) => ({id, ...rest, children: f (id)}),
(id, {parentId}) => parentId == id,
null
))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))</script>
或者
const input = [
{ forumId: 3, parentId: 1, forumName: "General", forumDescription: "General forum, talk whatever you want here", forumLocked: false, forumDisplay: true },
{ forumId: 2, parentId: 1, forumName: "Announcements", forumDescription: "Announcements & Projects posted here", forumLocked: false, forumDisplay: true },
{ forumId: 4, parentId: 3, forumName: "Introduction", forumDescription: "A warming introduction for newcomers here", forumLocked: false, forumDisplay: true },
{ forumId: 1, parentId: null, forumName: "Main", forumDescription: "", forumLocked: false, forumDisplay: true }
]
console .log (forest (
input,
(node, f) => ({...node, subforum: f(node .forumId)}),
(id, {parentId}) => parentId == id,
null
))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))</script>
输入结构明显不同
这些输入结构都相似,因为每个节点都指向其 parent 的标识符,当然根节点除外。但是这种技术与 parents 指向其 children 的标识符列表的技术一样有效。创建根元素(这里还有一个辅助函数)需要做更多的工作,但同样的系统将允许我们混合这样的模型:
const xs = [
{content: 'abc', wordpress_id: 196, child_ids: []},
{content: 'def', wordpress_id: 193, child_ids: [196, 199]},
{content: 'ghi', wordpress_id: 199, child_ids: []},
{content: 'jkl', wordpress_id: 207, child_ids: [208, 224]},
{content: 'mno', wordpress_id: 208, child_ids: [209]},
{content: 'pqr', wordpress_id: 209, child_ids: []},
{content: 'stu', wordpress_id: 224, child_ids: []}
]
const diff = (xs, ys) => xs .filter (x => !ys.includes(x))
console .log (forest (
xs,
(node, fn) => ({...node, children: fn(node)}),
({child_ids}, {wordpress_id}) => child_ids .includes (wordpress_id),
{child_ids: diff (xs .map (x => x .wordpress_id), xs .flatMap (x => x .child_ids))}
))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))</script>
这里我们有不同风格的isChild
,测试潜在child的id是否在parent提供的id列表中。为了创建初始根,我们必须扫描 id 列表,寻找那些没有显示为child 个 ID。我们使用 diff
帮助程序来执行此操作。
我在上面讨论额外的灵活性时提到了这种不同的风格。
只有第一关
我将此称为此类解决方案的“第一关”,因为这里有些地方我不是很满意。我们可以使用这种技术来处理删除 now-unnecessary parent id,并且如果实际上有实际要包含的 children 节点,也可以只包含一个 children
节点。对于原始示例,它可能如下所示:
console .log (forest (
edges,
( {node: {wordpress_id, wordpress_parent, ...rest}},
f,
kids = f (wordpress_id)
) => ({node: {
...rest,
wordpress_id,
...(kids.length ? {children: kids} : {})
}}),
(id, {node: {wordpress_parent}}) => wordpress_parent == id,
0
))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>const forest = (xs, build, isChild, root) => xs .filter (x => isChild (root, x)).map (node => build (node, root => forest (xs, build, isChild, root)))
const edges = [{node: {content: 'abc', wordpress_id: 196, wordpress_parent: 193}}, {node: {content: 'def', wordpress_id: 193, wordpress_parent: 0}}, {node: {content: 'ghi', wordpress_id: 199, wordpress_parent: 193}}, {node: {content: 'jkl', wordpress_id: 207, wordpress_parent: 0}}, {node: {content: 'mno', wordpress_id: 208, wordpress_parent: 207}}, {node: {content: 'pqr', wordpress_id: 209, wordpress_parent: 208}}, {node: {content: 'stu', wordpress_id: 224, wordpress_parent: 207}}]</script>
请注意,如果有内容,结果现在只包含 children
。 wordpress_parent
节点,现在是多余的,已被删除。
所以这可以用这种技术实现,我们可以对其他示例做类似的事情。但它在 build
函数中具有相当高的复杂性。我希望进一步反思可以产生一种简化这两个功能的方法。所以它仍在进行中。
结论
这种概括,将可重用 functions/modules 保存为个人工具包的一部分,可以极大地改进我们的代码库。我们刚刚将上面的相同函数用于许多明显相关但略有不同的行为。那只能是一场胜利。
这不是完整的代码,但它可以像这样使用,并且有多种改进途径可供追求。
非常感谢 shout-out 的启发。我可能早就应该这样做了,但不知何故这次它让我明白了。谢谢!
学习可重用模块和相互递归的好机会。此答案中的解决方案解决了您的特定问题,而无需对 input
示例 -
// Main.js
import { tree } from './Tree' // <- use modules!
const input =
[ { node: { content: 'abc', wordpress_id: 196, wordpress_parent: 193 } }
, { node: { content: 'def', wordpress_id: 193, wordpress_parent: null } } // <- !
, { node: { content: 'ghi', wordpress_id: 199, wordpress_parent: 193 } }
, { node: { content: 'jkl', wordpress_id: 207, wordpress_parent: null } } // <- !
, { node: { content: 'mno', wordpress_id: 208, wordpress_parent: 207 } }
, { node: { content: 'pqr', wordpress_id: 209, wordpress_parent: 208 } }
, { node: { content: 'stu', wordpress_id: 224, wordpress_parent: 207 } }
]
const result =
tree // <- make a tree
( input // <- array of nodes
, ({ node }) => node.wordpress_parent // <- foreign key
, ({ node }, child) => // <- node reconstructor function
({ node: { ...node, child: child(node.wordpress_id) } }) // <- primary key
)
console.log(JSON.stringify(result, null, 2))
输出-
[
{
"node": {
"content": "def",
"wordpress_id": 193,
"wordpress_parent": null,
"child": [
{
"node": {
"content": "abc",
"wordpress_id": 196,
"wordpress_parent": 193,
"child": []
}
},
{
"node": {
"content": "ghi",
"wordpress_id": 199,
"wordpress_parent": 193,
"child": []
}
}
]
}
},
{
"node": {
"content": "jkl",
"wordpress_id": 207,
"wordpress_parent": null,
"child": [
{
"node": {
"content": "mno",
"wordpress_id": 208,
"wordpress_parent": 207,
"child": [
{
"node": {
"content": "pqr",
"wordpress_id": 209,
"wordpress_parent": 208,
"child": []
}
}
]
}
},
{
"node": {
"content": "stu",
"wordpress_id": 224,
"wordpress_parent": 207,
"child": []
}
}
]
}
}
]
在input
中,我用wordpress_parent = null
表示一个根节点。如果需要,我们可以像在您的原始程序中一样使用 0
。 tree
接受第四个参数,root
,select 的节点作为树的基础。默认是 null
但我们可以指定 0
,比如 -
const input =
[ { node: { content: 'abc', wordpress_id: 196, wordpress_parent: 193 } }
, { node: { content: 'def', wordpress_id: 193, wordpress_parent: 0 } } // <- !
, { node: { content: 'ghi', wordpress_id: 199, wordpress_parent: 193 } }
, { node: { content: 'jkl', wordpress_id: 207, wordpress_parent: 0 } } // <- !
, { node: { content: 'mno', wordpress_id: 208, wordpress_parent: 207 } }
, { node: { content: 'pqr', wordpress_id: 209, wordpress_parent: 208 } }
, { node: { content: 'stu', wordpress_id: 224, wordpress_parent: 207 } }
]
const result =
tree
( input
, ({ node }) => node.wordpress_parent
, ({ node }, child) =>
({ node: { ...node, child: child(node.wordpress_id) } })
, 0 // <- !
)
console.log(JSON.stringify(result, null, 2))
// same output ...
为了使这个 post 完整,我将包含 Tree
模块的副本 -
// Tree.js
import { index } from './Index'
const empty =
{}
function tree (all, indexer, maker, root = null)
{ const cache =
index(all, indexer)
const many = (all = []) =>
all.map(x => one(x))
// zero knowledge of node shape
const one = (single) =>
maker(single, next => many(cache.get(next)))
return many(cache.get(root))
}
export { empty, tree } // <- public interface
和Index
模块依赖-
// Index.js
const empty = _ =>
new Map
const update = (r, k, t) =>
r.set(k, t(r.get(k)))
const append = (r, k, v) =>
update(r, k, (all = []) => [...all, v])
const index = (all = [], indexer) =>
all.reduce
( (r, v) => append(r, indexer(v), v) // zero knowledge of value shape
, empty()
)
export { empty, index, append } // <- public interface
如需更多见解,我鼓励您阅读