觉得前端不需要懂算法?来看看这个真实例子

380 阅读3分钟

需求背景

最近在开发需求中遇到了一个问题,前端需要利用antd的table组件来分层级展示一份数据,分层级是指可以类似树一样展开。

需要展示的数据的结构类似如下:

{
    'aaa': '111',
    'aaa.ccc': '222',
    'ddd.mmm': '333',
    'lll': '444',
    'lll.ffff.sss': '555',
    'ddd.mmm.bbb': '666',
    'xxx': '777'
}

树级展示类似官方文档所示:

image.png

官方文档示例代码

import React, { useState } from 'react'; 
import { Space, Switch, Table } from 'antd';
import type { ColumnsType } from 'antd/es/table'; 
import type { TableRowSelection } from 'antd/es/table/interface'; 

interface DataType { 
    key: React.ReactNode; 
    name: string; 
    age: number; 
    address: string; 
    children?: DataType[]; 
} 

const columns: ColumnsType<DataType> = [ 
    { title: 'Name', dataIndex: 'name', key: 'name', }, 
    { title: 'Age', dataIndex: 'age', key: 'age', width: '12%', }, 
    { title: 'Address', dataIndex: 'address', width: '30%', key: 'address', }, 
]; 

const data: DataType[] = [ 
    { 
        key: 1, 
        name: 'John Brown sr.', 
        age: 60, 
        address: 'New York No. 1 Lake Park', 
        children: [ 
            { 
                key: 11, 
                name: 'John Brown', 
                age: 42, 
                address: 'New York No. 2 Lake Park', 
            }, 
            {
                key: 12, 
                name: 'John Brown jr.', 
                age: 30, 
                address: 'New York No. 3 Lake Park', 
                children: [ 
                    { 
                        key: 121, 
                        name: 'Jimmy Brown', 
                        age: 16, 
                        address: 'New York No. 3 Lake Park', 
                    }, 
                ], 
            }, 
            { 
                key: 13, 
                name: 'Jim Green sr.', 
                age: 72, 
                address: 'London No. 1 Lake Park', 
                children: [ 
                    {
                        key: 131, 
                        name: 'Jim Green', 
                        age: 42, 
                        address: 'London No. 2 Lake Park', 
                        children: [ 
                            { 
                                key: 1311, 
                                name: 'Jim Green jr.', 
                                age: 25, 
                                address: 'London No. 3 Lake Park', 
                            }, 
                            { 
                                key: 1312, 
                                name: 'Jimmy Green sr.', 
                                age: 18, 
                                address: 'London No. 4 Lake Park', 
                            }, 
                        ], 
                    },
                ], 
            }, 
        ], 
    }, 
    { 
        key: 2, 
        name: 'Joe Black', 
        age: 32, 
        address: 'Sidney No. 1 Lake Park', 
    }, 
]; // rowSelection objects indicates the need for row selection 

const rowSelection: TableRowSelection<DataType> = { 
    onChange: (selectedRowKeys, selectedRows) => { 
        console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); 
    }, 
    onSelect: (record, selected, selectedRows) => { 
        console.log(record, selected, selectedRows); 
    }, 
    onSelectAll: (selected, selectedRows, changeRows) => { 
        console.log(selected, selectedRows, changeRows); 
    }, 
}; 

const App: React.FC = () => { 
    const [checkStrictly, setCheckStrictly] = useState(false); 
    return ( 
        <> 
            <Space align="center" style={{ marginBottom: 16 }}> 
                CheckStrictly: 
                <Switch checked={checkStrictly} onChange={setCheckStrictly} /> 
            </Space> 
            <Table columns={columns} rowSelection={{ ...rowSelection, checkStrictly }} dataSource={data} /> 
        </> 
    );
}; 
    
export default App;

看完示例代码后我们知道antd的table树级展示需要的数据结构是一个存在children这样一个key的多层嵌套树级数组。

需求分析

根据需求我们需要将

{
    'aaa': '111',
    'aaa.ccc': '222',
    'ddd.mmm': '333',
    'lll': '444',
    'lll.ffff.sss': '555',
    'ddd.mmm.bbb': '666',
    'xxx': '777'
}

这样一个object转成这样一个带有children的数组呢?

[
    {
        key: 'default',
        children: [
            {
                key: 'aaa',
                value: '111'
            },
            {
                key: 'lll',
                value: '444'
            },
            {
                key: 'xxx',
                value: '777'
            }
        ]
    },
    {
        key: 'aaa',
        children: [
            {
                key: 'aaa.ccc',
                value: '222'
            }
        ]
    },
    {
        key: 'ddd',
        children: [
            {
                key: 'ddd.mmm',
                value: '333'
            },
            {
                key: 'mmm',
                children: [
                    {
                        key: 'ddd.mmm.bbb',
                        value: '666'
                    }
                ]
            }
        ]
    },
    {
        key: 'lll',
        children: [
            {
                key: 'ffff',
                children: [
                    {
                        key: 'lll.ffff.sss',
                        value: '555'
                    }
                ]
            }
        ]
    }
]

分析:我们需要根据原数据object的key的 字符 '.' 来做切割,把不包含 '.' 的key放到 一个key为default的默认行里,其他按照 '.' 来做层级划分。

初实现

一开始我想的方法复杂很多,就是把原map 先递归转成一个有层级的 map,然后再递归转成树级数组。

// 将params转成paramslist
export function transformParams(params: ParamsType | undefined) {
  if (!params) return [];
  const newMap = dealParamsMap(params || {});
  return mapToList(newMap, [], 'parent');
}

// 将params递归分级
function dealParamsMap(map: { [key: string]: any }) {
  const newMap: { default?: any; [key: string]: any } = {};
  Object.keys(map).forEach((key) => {
    const keyArr = key.split('.');
    const value = map[key];
    if (keyArr.length === 1) {
      if (!newMap.default) {
        newMap.default = {};
      }
      newMap.default[key] = value;
    } else {
      changeKeyStrToMap(keyArr, key, value, newMap);
    }
  });
  return newMap;
}

// 将带.的key转成map
function changeKeyStrToMap(
  keyArr: Array<string>,
  key: string,
  value: string,
  map: {
    [key: string]: any;
  },
) {
  const keyStr = keyArr.shift();
  if (!keyArr.length) {
    map[key] = value;
    return map;
  }

  if (keyStr && !map[keyStr]) {
    map[keyStr] = {};
  }
  if (keyStr) changeKeyStrToMap(keyArr, key, value, map[keyStr]);
}

// map递归转成list
function mapToList(
  map: {
    [key: string]: any;
  },
  list: Array<ParamsType>,
  parentKey: string,
) {
  Object.keys(map).forEach((key) => {
    if (Object.prototype.toString.call(map[key]) === '[object Object]') {
      const oriKey = `${parentKey}-${key}`;
      list.push({
        key,
        oriKey,
        children: mapToList(map[key], [], oriKey),
        value: '',
      });
    } else {
      list.push({
        key,
        oriKey: key,
        value: map[key],
      });
    }
  });
  return list;
}

这里oriKey是用来作为antd table的row key唯一标识,大家可以自己定义,只要不重复就好。

虽然功能上没有问题,但是后面反回来看觉得两次递归还是多余,而且递归如果层级很深可能会出现爆栈,所以重新思考了下,能不能不使用递归就可以实现呢?

先思考了一下能不能一层递归实现,因为觉得转成树级map这一步看起来似乎多余了,再想了想,因为其实map在javasript中的存储其实都是引用地址,能不能利用这个引用地址来替代解决递归中把参数一层层往下传的问题呢?

其实我们看最终需要的那份数据的结构,有个关键的地方在于我们在处理某一个 key, value 的map的时候,需要找到他的父级,所以我们需要一个新map来存我们已经添加到最终数组的各个父级 map,然后通过我们自定义的一个key来找到它。

优化实现

// 把ab参数params的对象转成分层级的数组
export function transformParamsNew(params: { [key: string]: any } | undefined) {
  
  if (!params) return [];
  const newList = [];
  
  // 先处理准备放到default的key,也就是没有字符'.'的key
  const defaultValueKeys = Object.keys(params).filter((key) => {
    const keyArr = key.split('.');
    return keyArr.length === 1;
  });
  
  const defaultObject = {
    key: 'default',
    oriKey: 'parent-default',
    children: [] as any,
    value: '',
  };
  
  defaultValueKeys.forEach((key) => {
    const defaultValueObject = {
      key,
      oriKey: `default-${key}`,
      value: params[key],
    };
    defaultObject.children.push(defaultValueObject);
  });
  
  // 如果有划分到default的key,就把default的map添加到数组里
  if (defaultObject.children.length > 0) {
    newList.push(defaultObject);
  }

  // 处理不是default
  // 这是用来存储所有父级map的临时map
  const tempObject: { [key: string]: any } = {
    parent: { children: [] },
  };
  
  // 需要自定义一个key,用于antd table的row key
  let oriKey = '';
  Object.keys(params).forEach((key) => {
    const keyArr = key.split('.');
    const len = keyArr.length;
    // len > 1表示是有层级的key
    if (len > 1) {
      oriKey = 'parent';
      
      // 关键-- 这是保存上个处理的map(也就是父级map)的oriKey,用于在tempObject中找到父级map
      let prevOriKey = '';

      // 这里len - 1是因为我们需要的数据最后一个字map的key是原map完整的key,所以key最后一级单独处理
      for (let i = 0; i < len - 1; i += 1) {
        const keyItem = keyArr[i];
        oriKey = `${oriKey}-${keyItem}`;
        const valueObject = {
          key: keyItem,
          oriKey,
          children: [] as any,
        };

        // 如果tempObject中已存在当前map的父级map,则将当前map添加到父级map的children数组里
        if (prevOriKey && tempObject[prevOriKey] && !tempObject[oriKey]) {
          tempObject[prevOriKey].children.push(valueObject);
          
          // 排序,把有子数组的排前面,为了展示好看些, 可忽略
          tempObject[prevOriKey].children.sort((a: any, b: any) => {
            if (a.children && !b.children) {
              return 1;
            }
            if (!a.children && b.children) {
              return -1;
            }
            return 1;
          });
        }

        // 如果tempObject里不存在当前map,则将当前map添加到tempObject,便于下一级map找到
        if (!tempObject[oriKey]) {
          tempObject[oriKey] = valueObject;
          
          // 如果是key里第一级,则先放到tempObject的parent的子数组中
          if (i === 0) {
            tempObject.parent.children.push(valueObject);
           
            tempObject.parent.children.sort((a: any, b: any) => {
              if (a.children && !b.children) {
                return 1;
              }
              if (!a.children && b.children) {
                return -1;
              }
              return 1;
            });
          }
          prevOriKey = oriKey;
        } else {
          // 如果存在则说明已存在同样map,避免重复
          prevOriKey = oriKey;
          continue;
        }
      }
      // 遍历结束
      
      // 单独处理key的最后一级
      if (tempObject[prevOriKey]) {
        const valueObject = {
          key,
          oriKey: key,
          value: params[key],
        };
        tempObject[prevOriKey].children.push(valueObject);
        tempObject[prevOriKey].children.sort((a: any, b: any) => {
          if (a.children && !b.children) {
            return 1;
          }
          if (!a.children && b.children) {
            return -1;
          }
          return 1;
        });
      }
    }
  });

  // 因为javascript对象存储的是引用地址,所以tempObject的parent的子数组就是我们想要的层级数据了
  if (tempObject.parent) {
    newList.push(...tempObject.parent.children);
  }
  return newList;
}

结果展示:

image.png

也达到我们想要的效果啦!!🎉

如果有大佬有更好的方法实现的话,欢迎到评论区贴一下,教教我这个菜🐔哈~