【问题标题】:Convert array of flat objects to nested objects将平面对象数组转换为嵌套对象
【发布时间】:2019-02-06 15:44:04
【问题描述】:

我有以下数组(实际上来自后端服务):

const flat: Item[] = [
    { 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' }
];

Item 在哪里:

interface Item {
    id: string;
    name: string;
    parentId: string;
};

为了兼容显示树(类文件夹)视图的组件,需要将其转换为:

const treeData: NestedItem[] = [
    {
        id: 'a',
        name: 'Root 1',
        root: true,
        count: 2,
        children: [
          {
            id: 'a1',
            name: 'Item 1'
          },
          {
            id: 'a2',
            name: 'Item 2'
          }
        ]
    },
    {
        id: 'b',
        name: 'Root 2',
        root: true,
        count: 5, // number of all children (direct + children of children)
        children: [
          {
            id: 'b1',
            name: 'Item 1'
          },
          {
            id: 'b2',
            name: 'Item 2',
            count: 2,
            children: [
                { id: 'b2-1', name: 'Item 2-1' },
                { id: 'b2-2', name: 'Item 2-2' },
            ]
          },
          {
            id: 'b3',
            name: 'Item 3'
          },
        ]
    },
    {
        id: 'c',
        name: 'Root 3',
        root: true,
        count: 2,
        children: [
          {
            id: 'c1',
            name: 'Item 1'
          },
          {
            id: 'c2',
            name: 'Item 2'
          }
        ]
    }
];

NestedItem 在哪里:

interface NestedItem {
    id: string;
    name: string;
    root?: boolean;
    count?: number;
    children?: NestedItem[];
}

到目前为止,我所尝试的只是:

// Get roots first
const roots: NestedItem[] = flat
    .filter(item => !item.parentId)
    .map((item): NestedItem => {
        return { id: item.id, name: item.name, root: true }
    });

// Add "children" to those roots
const treeData = roots.map(node => {
    const children = flat
        .filter(item => item.parentId === node.id)
        .map(item => {
            return { id: item.id, name: item.name }
        });
    return {
        ...node,
        children,
        count: node.count ? node.count + children.length : children.length
    }
});

但这当然只能获得第一级子节点(根节点的直接子节点)。它以某种方式需要递归,但我不知道如何实现。

【问题讨论】:

    标签: javascript typescript recursion nested


    【解决方案1】:

    不对展平数组的顺序或嵌套对象的深度做任何假设:

    Array.prototype.reduce 足够灵活来完成这项工作。如果你不熟悉Array.prototype.reduce,我推荐reading this。您可以通过执行以下操作来完成此操作。

    我在这里有两个依赖递归的函数:findParent 和 checkLeftOvers。 findParent 尝试查找对象父对象并根据是否找到它返回true 或false。在我的减速器中,如果findParent 返回false,我将当前值添加到剩余数组中。如果findParent 返回true,我调用checkLeftOvers 来查看我的剩余数组中是否有任何对象是刚刚添加的对象findParent 的子对象。

    注意:我将{ id: 'b2-2-1', name: 'Item 2-2-1', parentId: 'b2-2'} 添加到flat 数组中,以证明这将达到您想要的深度。我还重新排序了flat,以证明这在这种情况下也可以工作。希望这会有所帮助。

    const flat = [
        { id: 'a2', name: 'Item 1', parentId: 'a' },
        { id: 'b2-2-1', name: 'Item 2-2-1', parentId: 'b2-2'},
        { id: 'a1', name: 'Item 1', parentId: 'a' },
        { id: 'a', name: 'Root 1', parentId: null },
        { id: 'b', name: 'Root 2', parentId: null },
        { id: 'c', name: 'Root 3', parentId: null },
        { 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' }
    ];
    
    function checkLeftOvers(leftOvers, possibleParent){
      for (let i = 0; i < leftOvers.length; i++) {
        if(leftOvers[i].parentId === possibleParent.id) {
          delete leftOvers[i].parentId
          possibleParent.children ? possibleParent.children.push(leftOvers[i]) : possibleParent.children = [leftOvers[i]]
          possibleParent.count = possibleParent.children.length
          const addedObj = leftOvers.splice(i, 1)
          checkLeftOvers(leftOvers, addedObj[0])
        }
      }
    }
    
    function findParent(possibleParents, possibleChild) {
      let found = false
      for (let i = 0; i < possibleParents.length; i++) {
        if(possibleParents[i].id === possibleChild.parentId) {
          found = true
          delete possibleChild.parentId
          if(possibleParents[i].children) possibleParents[i].children.push(possibleChild)
          else possibleParents[i].children = [possibleChild]
          possibleParents[i].count = possibleParents[i].children.length
          return true
        } else if (possibleParents[i].children) found = findParent(possibleParents[i].children, possibleChild)
      } 
      return found;
    }
     
     const nested = flat.reduce((initial, value, index, original) => {
       if (value.parentId === null) {
         if (initial.left.length) checkLeftOvers(initial.left, value)
         delete value.parentId
         value.root = true;
         initial.nested.push(value)
       }
       else {
          let parentFound = findParent(initial.nested, value)
          if (parentFound) checkLeftOvers(initial.left, value)
          else initial.left.push(value)
       }
       return index < original.length - 1 ? initial : initial.nested
     }, {nested: [], left: []})
     
    console.log(nested)

    【讨论】:

    • 很抱歉迟到了,我已经离开了一段时间。现在回到它。谢谢! “不做任何假设”总是最好的假设:)
    • 很高兴我能帮上忙。快乐编码:)
    【解决方案2】:

    假设平面项目数组总是像您的情况一样排序(父节点在子节点之前排序)。下面的代码应该可以完成这项工作。

    首先,我使用数组上的 reduce 构建没有 count 属性的树,以构建一个映射来跟踪每个节点并将父节点链接到子节点:

    type NestedItemMap = { [nodeId: string]: NestedItem };
    
    let nestedItemMap: NestedItemMap = flat
        .reduce((nestedItemMap: NestedItemMap, item: Item): NestedItemMap => {
    
            // Create the nested item
            nestedItemMap[item.id] = {
                id: item.id,
                name: item.name
            }
    
            if(item.parentId == null){
                // No parent id, it's a root node
                nestedItemMap[item.id].root = true;
            }
            else{
                // Child node
                let parentItem: NestedItem = nestedItemMap[item.parentId];
    
                if(parentItem.children == undefined){
                    // First child, create the children array
                    parentItem.children = [];
                    parentItem.count = 0;
    
                }
    
                // Add the child node in it's parent children
                parentItem.children.push(
                    nestedItemMap[item.id]
                );
                parentItem.count++;
            }
    
            return nestedItemMap;
        }, {});
    

    在减少数组时,父节点始终排在第一位这一事实确保了在构建子节点时父节点在nestedItemMap 中可用。

    这里有树,但没有 count 属性:

    let roots: NestedItem[] = Object.keys(nestedItemMap)
        .map((key: string): NestedItem => nestedItemMap[key])
        .filter((item: NestedItem): boolean => item.root);
    

    要填充 count 属性,我个人更喜欢在树上执行后序深度优先搜索。但在您的情况下,由于节点 ID 命名(排序后,父节点 ID 排在第一位)。您可以使用以下方法计算它们:

    let roots: NestedItem[] = Object.keys(nestedItemMap)
        .map((key: string): NestedItem => nestedItemMap[key])
        .reverse()
        .map((item: NestedItem): NestedItem => {
            if(item.children != undefined){
                item.count = item.children
                    .map((child: NestedItem): number => {
                        return 1 + (child.count != undefined ? child.count : 0);
                    })
                    .reduce((a, b) => a + b, 0);
            }
    
            return item;
        })
        .filter((item: NestedItem): boolean => item.root)
        .reverse();
    

    我只是反转数组以首先获取所有子项(例如在后序 DFS 中),然后计算 count 值。 最后一个反转在这里只是为了像你的问题一样排序:)。

    【讨论】:

      【解决方案3】:

      您可以采用标准方法来处理树,它采用单个循环并存储子与父之间以及父与子之间的关系。

      要获得根属性,您需要进行额外检查。

      然后采用迭代和递归的方法来获取计数。

      var data = [{ 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: 'b3', name: 'Item 3', parentId: 'b' }, { id: 'c1', name: 'Item 1', parentId: 'c' }, { id: 'c2', name: 'Item 2', parentId: 'c' }, { id: 'b2-1', name: 'Item 2-1', parentId: 'b2' }, { id: 'b2-2', name: 'Item 2-2', parentId: 'b2' },],
          tree = function (data, root) {
      
              function setCount(object) {
                  return object.children
                      ? (object.count = object.children.reduce((s, o) => s + 1 + setCount(o), 0))
                      : 0;
              }                
      
              var t = {};
              data.forEach(o => {
                  Object.assign(t[o.id] = t[o.id] || {}, o);
                  t[o.parentId] = t[o.parentId] || {};
                  t[o.parentId].children = t[o.parentId].children || [];
                  t[o.parentId].children.push(t[o.id]);
                  if (o.parentId === root) t[o.id].root = true;          // extra
              });
      
              setCount(t[root]);                                         // extra
              return t[root].children;
          }(data, null);
      
      console.log(tree);
      .as-console-wrapper { max-height: 100% !important; top: 0; }

      【讨论】:

        【解决方案4】:

        也许这对你有帮助,输入是平面 obj

        nestData = (data, parentId = '') => {
        return data.reduce((result, fromData) => {
          const obj = Object.assign({}, fromData);
        
          if (parentId === fromData.parent_id) {
            const children = this.nestData(data, fromData.id);
            if (children.length) {
              obj.children = children;
            } else {
              obj.userData = [];
            }
            result.push(obj);
          }
          return result;
        }, []);
        

        };

        【讨论】:

        • 这可能取自一个不同但相关的任务的代码。需要清理才能解决这个问题。独立时,您需要从对nestData 的递归调用中删除this。您需要将parent_id 更改为parentId,并且由于userData 与此问题无关,因此可以删除else 块。但是,最大的失误是它不包括请求的 count 或 root 属性。 root 很容易,但 count 需要一些工作。一个相关的方法是在最后一次更新到another answer。
        【解决方案5】:

        如果您事先掌握了这么多信息,则可以更轻松地向后构建树。由于您非常了解输入的形状并且它们的关系已明确定义,您可以轻松地将其分成多个数组并自下而上构建它:

        function buildTree(arr: Item[]): NestedItem[] {
          /* first split the input into separate arrays based on their nested level */
          const roots = arr.filter(r => /^\w{1}$/.test(r.id));
          const levelOne = arr.filter(r => /^\w{1}\d{1}$/.test(r.id));
          const levelTwo = arr.filter(r => /^\w{1}\d{1}-\d{1}$/.test(r.id));
        
          /* then create the bottom most level based on their relationship to their parent*/
          const nested = levelOne.map(item => {
            const children = levelTwo.filter(c => c.parentId === item.id);
            if (children) {
              return {
                ...item,
                count: children.length,
                children
              };
            } else return item;
          });
        
          /* and finally do the same with the root items and return the result */
          return roots.map(item => {
            const children = nested.filter(c => c.parentId === item.id);
            if (children) {
              return {
                ...item,
                count: children.length,
                children,
                root: true
              };
            } else return { ...item, root: true };
          });
        }
        

        这可能不是最高效的解决方案,它需要根据输入的预期形状进行一些调整,但它是一个干净且可读的解决方案。

        【讨论】:

          【解决方案6】:

          另一种方法可能如下所示:

          const countKids = (nodes) => 
            nodes.length + nodes.map(({children = []}) => countKids(children)).reduce((a, b) => a + b, 0)
          
          const makeForest = (id, xs) => 
            xs .filter (({parentId}) => parentId == id)
               .map (({id, parentId, ...rest}) => {
                 const kids = makeForest (id, xs)
                 return {id, ...rest, ...(kids .length ? {count: countKids (kids), children: kids} : {})}
               })
          
          const nest = (flat) =>
            makeForest (null, flat)
              .map ((node) => ({...node, root: true}))
          
          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 (nest (flat))
          .as-console-wrapper {min-height: 100% !important; top: 0}

          主函数 (makeForest) 查找所有 id 与目标匹配的子项(最初为 null),然后递归地对这些子项的 id 执行相同操作。

          这里唯一的复杂性是如果节点的子节点为空,则不包括 count 或 children。如果包括它们不是问题,那么这可以简化。

          【讨论】:

            【解决方案7】:
            this.treeData = this.buildTreeData(
                flat.filter(f => !f.parentId), flat
            );
            
            private buildTreeData(datagroup: Item[], flat: Item[]): any[] {
                return datagroup.map((data) => {
                    const items = this.buildTreeData(
                        flat.filter((f) => f.parentId === data.id), flat
                  );
                  return {
                      ...data,
                      root: !data.parentId,
                      count: items?.length || null
                      children: items,
                  };
              });
            }
            

            【讨论】:

            • 请添加更多详细信息以扩展您的答案,例如工作代码或文档引用。
            【解决方案8】:

            您好,我尝试了 Cody 接受的答案,但在数据未排序以及级别>2 的嵌套数据时遇到了一些问题

            1. 在这个沙盒中: https://codesandbox.io/s/runtime-dew-g48sk?file=/src/index.js:1875-1890 我只是稍微改变了顺序(id=3 被移到列表的末尾),看看我们现在如何在控制台中得到 c 只有 1 个孩子

            2. 我遇到了另一个找不到父母的问题,因为在findParent 函数中,如果函数被递归调用且第一个参数是一个大于 1 的数组(例如找到一个id=21 的父级:

              {id: 1,parentId: null, children: [
                  {
                    id: 10,
                    parentId: 1,
                    children: []
                  },
                  {
                    id: 11,
                    parentId: 1,
                    children: [{
                      id: 21...
                    }]
                  }
              ]}
              

              会失败

            无论如何,我认为流程本身很好,只是需要一些小的修复和重命名,所以这对我有用,我删除了一些我没有使用的属性(如counter)并添加了一些我自己的(像expanded),但显然这根本不重要,我也在使用TS(但我将所有类型都更改为any):

            class NestService {
              public nestSearchResultsToTree(flatItemsPath: any[]) {
                const nested = flatItemsPath.reduce(
                  (
                    initial: { nested: any[]; left: any[] },
                    value: any,
                    index: number,
                    original: any
                  ) => {
                    if (value.parentId === null) {
                      if (initial.left.length) this.checkLeftOvers(initial.left, value);
                      initial.nested.push(value);
                    } else {
                      const parentFound = this.findParent(initial.nested, value);
                      if (parentFound) this.checkLeftOvers(initial.left, value);
                      else initial.left.push(value);
                    }
                    return index < original.length - 1 ? initial : initial.nested;
                  },
                  { nested: [], left: [] }
                );
                return nested;
              }
            
              private checkLeftOvers(leftOvers: any[], possibleParent: any) {
                for (let i = 0; i < leftOvers.length; i++) {
                  const possibleChild = leftOvers[i];
                  if (possibleChild.id === possibleParent.id) continue;
                  if (possibleChild.parentId === possibleParent.id) {
                    possibleParent.children
                      ? possibleParent.children.push(possibleChild)
                      : (possibleParent.children = [possibleChild]);
                    possibleParent.expanded = true;
                    possibleParent.isFetched = true;
                    this.checkLeftOvers(leftOvers, possibleChild);
                  }
                }
              }
            
              private findParent(
                possibleParents: any,
                child: any,
                isAlreadyFound?: boolean
              ): boolean {
                if (isAlreadyFound) return true;
                let found = false;
                for (let i = 0; i < possibleParents.length; i++) {
                  const possibleParent = possibleParents[i];
                  if (possibleParent.id === child.parentId) {
                    possibleParent.expanded = true;
                    possibleParent.isFetched = true;
                    found = true;
                    if (possibleParent.children) possibleParent.children.push(child);
                    else possibleParent.children = [child];
                    return true;
                  } else if (possibleParent.children)
                    found = this.findParent(possibleParent.children, child, found);
                }
                return found;
              }
            }

            【讨论】:

              猜你喜欢
              • 2020-01-15
              • 1970-01-01
              • 2016-04-03
              • 1970-01-01
              • 2019-05-14
              • 2016-11-16
              • 2018-01-04
              • 2019-12-02
              相关资源
              最近更新 更多