数组转化为树结构

119 阅读3分钟

数组转化为树

我们在做后台管理系统的项目时候经常用到一个问题就是权限问题。比如前端的路由,我们从后端获取的数据经常是以数组的形式返回的。

比如:

"menu_list": [{"id":1,"menu_name":"首页","pid":-1,"logo":null,"rank":1,"link":"home"},{"id":2,"menu_name":"企业信息","pid":-1,"logo":null,"rank":2,"link":"manager"},{"id":3,"menu_name":"财务管理","pid":-1,"logo":null,"rank":4,"link":"finance"},{"id":4,"menu_name":"订单管理","pid":-1,"logo":null,"rank":5,"link":"bills"},{"id":5,"menu_name":"设置","pid":-1,"logo":null,"rank":8,"link":"setting"},{"id":6,"menu_name":"员工管理","pid":2,"logo":null,"rank":1,"link":"manager/employee/list"},{"id":7,"menu_name":"角色管理","pid":2,"logo":null,"rank":2,"link":"manager/role/list"},{"id":8,"menu_name":"企业钱包","pid":3,"logo":null,"rank":1,"link":"finance/wallet"},{"id":9,"menu_name":"财务对账","pid":3,"logo":null,"rank":2,"link":"finance/check"},{"id":10,"menu_name":"系统设置","pid":5,"logo":null,"rank":1,"link":"setting/system"},{"id":17,"menu_name":"用车报告","pid":-1,"logo":null,"rank":6,"link":"report"},{"id":18,"menu_name":"下单约车","pid":-1,"logo":null,"rank":7,"link":"createOrder"},{"id":19,"menu_name":"部门管理","pid":2,"logo":null,"rank":3,"link":"manager/department"},{"id":20,"menu_name":"项目管理","pid":2,"logo":null,"rank":4,"link":"manager/projects"},{"id":21,"menu_name":"用车管控","pid":-1,"logo":null,"rank":3,"link":"manage_control"},{"id":22,"menu_name":"用车制度","pid":21,"logo":null,"rank":1,"link":"manage_control_list"}]

我们需要将这种数据转化为前端可用的数据,树的结构。

我们就需要一个方法,ArrayToTree

我们先说说传统的方法:

我们就使用上边的数据:我们定义 arr等于menu_list

方法一:递归实现

/**
* 递归查找,获取children
*/
const getChildren = (data, result, pid) => {
  for (const item of data) {
    if (item.pid === pid) {
      const newItem = { ...item, children: [] };
      result.push(newItem);
      getChildren(data, newItem.children, item.id);
    } else if (item.id === -1) {
      result.push(item);
    }
  }
};
​
/**
         * 转换方法
         */
const arrayToTree = (data, pid) => {
  const result = [];
  getChildren(data, result, pid);
  return result;
};
​
console.log(arrayToTree(arr, -1));

递归查找其实不是一种好的方式,因为递归性能比较差。

方法二:我们通过Map实现

主要思路是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储

function arrayToTree(items) {
  const result = []; // 存放结果集
  const itemMap = {}; //
​
  // 先转成map存储
  for (const item of items) {
    itemMap[item.id] = { ...item, children: [] };
  }
​
  for (const item of items) {
    const id = item.id;
    const pid = item.pid;
    const treeItem = itemMap[id];
    if (pid === -1) {
      result.push(treeItem);
    } else {
      if (!itemMap[pid]) {
        itemMap[pid] = {
          children: [],
        };
      }
      itemMap[pid].children.push(treeItem);
    }
  }
  return result;
}

方式三:性能最优

主要思路也是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储。不同点在遍历的时候即做Map存储,有找对应关系。性能会更好。

function arrayToTree(items) {
  const result = []; // 存放结果集
  const itemMap = {}; //
  
  // 遍历数组
  for (const item of items) {
    const id = item.id;
    const pid = item.pid;
    
    // 如果对应的 map 对象里边没有 我们给一个初始值
    if (!itemMap[id]) {
      itemMap[id] = {
        children: [],
      };
    }
    
    // 我们将数值给到 map 数据
    itemMap[id] = {
      ...item,
      children: itemMap[id]["children"],
    };
    
    // 我们拿到 map 中对应的数据
    const treeItem = itemMap[id];
    
    // 如果没有父级
    if (pid === -1) {
      result.push(treeItem);
    } else {
      if (!itemMap[pid]) {
        itemMap[pid] = {
          children: [],
        };
      }
      // 如果有父级,我们直接给父级里边存放
      itemMap[pid].children.push(treeItem);
    }
  }
  return result;
}

这里我从网上找了一个 github 上的插件:

里边的实现方式如下:

var property = require('nested-property');
var keyBy = require('lodash.keyby');
​
​
function createTree(array, rootNodes, customID, childrenProperty) {
  var tree = [];
​
  for (var rootNode in rootNodes) {
    var node = rootNodes[rootNode];
    var childNode = array[node[customID]];
​
    if (!node && !rootNodes.hasOwnProperty(rootNode)) {
      continue;
    }
​
    if (childNode) {
      node[childrenProperty] = createTree(
        array,
        childNode,
        customID,
        childrenProperty
      );
    }
​
    tree.push(node);
  }
​
  return tree;
}
​
function groupByParents(array, options) {
  var arrayByID = keyBy(array, options.customID);
​
  return array.reduce(function(prev, item) {
    var parentID = property.get(item, options.parentProperty);
    if (!parentID || !arrayByID.hasOwnProperty(parentID)) {
      parentID = options.rootID;
    }
​
    if (parentID && prev.hasOwnProperty(parentID)) {
      prev[parentID].push(item);
      return prev;
    }
​
    prev[parentID] = [item];
    return prev;
  }, {});
}
​
function isObject(o) {
  return Object.prototype.toString.call(o) === '[object Object]';
}
​
function deepClone(data) {
  if (Array.isArray(data)) {
    return data.map(deepClone);
  } else if (isObject(data)) {
    return Object.keys(data).reduce(function(o, k) {
      o[k] = deepClone(data[k]);
      return o;
    }, {});
  } else {
    return data;
  }
}
​
/**
 * arrayToTree
 * Convert a plain array of nodes (with pointers to parent nodes) to a nested
 * data structure
 *
 * @name arrayToTree
 * @function
 *
 * @param {Array} data An array of data
 * @param {Object} options An object containing the following fields:
 *
 *  - `parentProperty` (String): A name of a property where a link to
 * a parent node could be found. Default: 'parent_id'
 *  - `customID` (String): An unique node identifier. Default: 'id'
 *  - `childrenProperty` (String): A name of a property where children nodes
 * are going to be stored. Default: 'children'.
 *
 * @return {Array} Result of transformation
 */module.exports = function arrayToTree(data, options) {
  options = Object.assign(
    {
      parentProperty: 'parent_id',
      childrenProperty: 'children',
      customID: 'id',
      rootID: '0'
    },
    options
  );
​
  if (!Array.isArray(data)) {
    throw new TypeError('Expected an array but got an invalid argument');
  }
​
  var grouped = groupByParents(deepClone(data), options);
  return createTree(
    grouped,
    grouped[options.rootID],
    options.customID,
    options.childrenProperty
  );
}