如何使用递归 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 正好等同于该答案的 makerxs 等同于 allroot 参数(几乎)等同。 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。但是当我们这样做时,参数 isChildbuild 提供的回调会更简单一些。要记住的是,这是传递给 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>

请注意,如果有内容,结果现在只包含 childrenwordpress_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表示一个根节点。如果需要,我们可以像在您的原始程序中一样使用 0tree 接受第四个参数,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

如需更多见解,我鼓励您阅读