如何使用递归 JavaScript 映射方法从父/子关系创建新对象

IT技术 javascript reactjs recursion nested map-function
2021-04-05 21:16:10

我有一个对象数组。其中一些wordpress_parent有一个值为`props。这意味着这个节点是另一个节点的子节点。实际的最终结果是一个嵌套的评论 UI,因此可以有多个级别的子级。

在此处输入图片说明

我想遍历我的对象和 where wordpress_parent !== 0,在原始数组中找到wordpress_id等于 的值的wordpress_parent对象,并将该对象作为匹配父节点的子属性。

我想实现这个对象形式:

node {
    ...originalPropsHere,
    children: { ...originalChildNodeProps } 
}

这个想法是创建一个新数组,它具有正确的父级和子级嵌套结构,然后我可以对其进行迭代并输出到 JSX 结构中。

我想编写一个递归函数来执行此逻辑,然后返回一个像这样的 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>

我想我必须使用 JavaScripts 的map方法来创建一个新数组。我遇到的麻烦是操作数据以children在我的父节点上创建一个新属性,然后将匹配的子注释作为该属性的值。然后将它放在一个漂亮的小函数中,该函数递归执行并创建我可以在我的组件中呈现的 HTML/JSX 结构。

聪明的人,请站出来,谢谢!:D

2个回答

这是对另一个答案的修改,它处理额外的node包装器以及您的 id 和父属性名称:

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在那些没有子节点的节点中包含一个空数组。(如果你知道你最多只能有一个孩子并且更喜欢这个名字child,那么这需要一些修改;但它应该不错。)

基本上,它需要一个项目列表和一个 id 来测试,并过滤具有该 id 的列表。然后它children通过递归调用具有当前对象的 id 的函数来添加一个属性。

由于wordpress_parent包含在传递给map但未包含在输出中的函数的解构参数中,因此跳过此节点。如果你想保留它,你可以将它添加到输出中,但更容易的是将它作为参数跳过;那么它将成为...rest.

更新:概括

来自Thankyou回答是鼓舞人心的。我已经用相同答案的变体回答了很多这样的问题。现在是泛化到可重用功能的时候了。

该答案创建所有值的索引,然后使用该索引构建输出。我上面的技术(以及其他几个答案)有些不同:扫描所有根元素的数组,对于每个根元素,扫描其子元素的数组,以及为每个子元素扫描数组,等等。这可能效率较低,但更容易推广,因为不需要为每个元素生成代表键。

所以我在一个更通用的解决方案中创建了第一遍,其中我上面所做的被分离成两个更简单的函数,这些函数被传递(连同原始数据和根值的表示)到一个通用函数中,该函数将将它们放在一起并处理递归。

以下是使用此函数解决当前问题的示例:

// 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'sindexer和我的isChild. 因为Thankyou 生成元素的外键映射,indexer获取一个节点并返回该节点的表示,通常是一个属性。我的版本是一个二进制谓词。它接受当前元素和第二个元素的表示,并true当且仅当第二个元素是当前元素的子元素时才返回

不同风格的root参数

最后一个参数 ,root实际上相当有趣。它需要是当前对象的某种代表。但它不需要是任何特定的代表。在简单的情况下,这可能只是一个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>

在这种情况下,最后一个参数更复杂,它是一个结构类似于列表中典型元素的对象,在这种情况下具有根 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>

或者,Thankyou 提供示例可能如下所示:

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>

明显不同的输入结构

这些输入结构的相似之处在于每个节点都指向其父节点的标识符,当然根节点除外。但是这种技术同样适用于父母指向他们孩子的标识符列表的技术。创建根元素(这里还有一个辅助函数)需要更多的工作,但相同的系统将允许我们对这样的模型进行水合:

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,测试潜在孩子的 id 是否在父母提供的 id 列表中。并且为了创建初始根,我们必须扫描那些没有作为子 ID 出现的 ID 列表。我们使用一个diff助手来做到这一点。

这种不同的风格就是我在上面讨论额外灵活性时提到的。

只有第一关

我将其称为此类解决方案的“第一次通过”,因为这里有一些我不太满意的地方。我们可以使用这种技术来处理删除现在不需要的父 ID,并且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函数中具有相当高的复杂性我希望进一步的反思可以产生一种简化这两个特征的方法。所以它仍在进行中。

结论

这种概括,将这种可重用的功能/module保存为个人工具包的一部分,可以极大地改进我们的代码库。我们刚刚对许多明显相关但略有不同的行为使用了上述相同的函数。那只能是一场胜利。

这不是完整的代码,但它可以像这样使用,并且有几个改进的途径可以追求。

感谢您的灵感。我可能早就应该这样做了,但这次不知何故它传到了我身上。谢谢!

我在第一次出现递归时遇到了问题。我的函数方法内部的属性分配中printComments被分配如下:不是数组,因此该方法会引发错误。本来我打电话从我Component内我的功能渲染功能:来自数据层的原始对象是. 第一次迭代工作正常,但我的对象是. 我相信类型更改 (?) 将其抛出。childrenmapprintComments(comments.edges, wordpress_id)commentsfilter{printComments(postComments)}postComments{edges: Array(x)}childrennull
2021-05-27 21:16:10
添加了一个很长的概括部分,灵感来自@Thankyou。
2021-06-07 21:16:10
只是有机会阅读您的更新。Map可提供大型数据集的性能改进,但它是很难被击败的俏皮话像forest我很高兴看到你filter像那样使用和谓词。我喜欢它的阅读方式。写得很好 :D
2021-06-13 21:16:10
谢谢@Scott Sauyet,这几乎正是我想要的。简洁并使用 ES6。我无法消除那些嵌套的解构参数。为你点赞!
2021-06-17 21:16:10

了解可重用module和相互递归的绝佳机会。此答案中的此解决方案解决了您的特定问题,而无需对另一个答案中编写的module进行任何修改@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如果需要,我们可以在您的原始程序中使用like。tree接受第四个参数 ,root即要选择作为树基础的节点。默认是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 ...

为了使这篇文章完整,我将包含该Treemodule的副本-

// 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

Indexmodule依赖 -

// 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

为了获得更多见解,我鼓励您阅读原始问答

嘿,非常酷的解决方案,并且解释得很清楚。谢谢你。
2021-05-29 21:16:10