最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • JS树形结构处理 | 七日打卡

    正文概述 掘金(一个卷er)   2021-01-17   507

    前言

    在开发 UI 组件,如侧栏菜单、级联选择器、表格、树形选择器等的过程中,我们经常会遇到需要处理数据结构——Tree的情况,但是每次写起来却觉得没那么简单或者代码看起来不够优雅。这里,我就结合一些源码谈谈常见的树形结构处理的一些技巧。

    基础知识

    DFS(deep-first-search 深度优先遍历)和 BFS(breath-first-search 广度优先遍历)都是常见的搜索算法。往往和递归、遍历、回溯的概念相关联。在树形结构的处理中,我们往往结合这两种算法处理。 我们来看一个经典的树形结构并思考以下问题:

    const data = {
      value: 0,
      children: [
        {
          value: 1,
          children: [
            { value: 11 },
            { value: 12 },
          ],
        },
        {
          value: 2,
          children: [
            { value: 21 },
          ],
        },
      ],
    };
    
    ? 深度优先遍历的结果是?
    // [0, 1, 11, 12, 2, 21]
    
    ? 广度优先遍历的结果是?
    // [0, 1, 2, 11, 12, 21]
    

    深度优先遍历

    深度优先即从根节点向下遍历,一条路径走完再查找另一条路径,通常可以结合“栈”这个数据结构进行处理。

    • 迭代解法
    function dfs_iterate(tree, callback) {
      let stack = [...tree];
      while (stack.length) {
        let node = stack.pop();
        callback(node.value);
        if (node.children) {
          // 思考,先进后出,要保证列表顺序,需要反转数组
          stack.push(...node.children.reverse());
        }
      }
    }
    
    • 递归解法
    function dfs(data, callback) {
      // 递归是天然的堆栈
      data.forEach((node) => {
        // 前序 or 后序 取决于这里先遍历根还是孩子节点
        callback(node.value);
        if (node.children) {
          dfs(node.children, callback);
        }
      });
    }
    

    广度优先遍历

    广度优先即从根节点向下遍历,先找到所有最近的子节点,再向下查找,通常可以结合“队列”这个数据结构进行处理。

    • 迭代解法
    function bfs_iterate(tree, callback) {
      let queue = [...tree];
      while (queue.length) {
        let node = queue.shift();
        callback(node.value);
        if (node.children) {
          queue.push(...node.children);
        }
      }
    }
    
    • 递归解法

    通常我们不采用递归进行广度优先遍历,因为递归是天然的栈,但是这里我们可以通过一个 level 参数标记当前深度转化为深度递归遍历,相当于层次遍历

    function bfs(tree) {
      let result = [];
      dfs(tree, 0);
      function dfs(tree, level) {
        tree.forEach((node) => {
          if (result.length <= level) {
            result.push([]);
          }
          result[level].push(node.value);
          if (node.children) {
            dfs(node.children, level + 1);
          }
        });
      }
      return result;
    }
    

    扁平数组转树形结构

    这里我们先通过一个例子来简单分析一下:

    const data = [
       { id: 'node-1', parent: 'root' },
       { id: 'node-2', parent: 'root' },
       { id: 'node-3', parent: 'node-2' },
       { id: 'node-4', parent: 'node-2' },
       { id: 'node-5', parent: 'node-4' },
     ]
    

    想想构建这棵树我们要怎么做呢?最容易理解的就是至上而下构建的过程,很直观的:

    • 我们可以推测,最终构建的树的根节点是id === node-1 || node-2
    • node-1 没有子节点
    • node-2 的子节点是id === node-3 || node-4
    • node-3 没有子节点
    • node-4 的子节点是id === node-5
    • node-5 没有子节点

    最终,我们可以得到这棵树的结果是:

    [
      { id: "node-1", parent: "root" },
      {
        id: "node-2",
        parent: "root",
        children: [
          { id: "node-3", parent: "node-2" },
          {
            id: "node-4",
            parent: "node-2",
            children: [{ id: "node-5", parent: "node-4" }],
          },
        ],
      },
    ];
    

    理论

    那么,我们如何将这个构建的过程抽象化为可执行的步骤呢?这里我们将树构建的过程拆分为三个阶段:

    • 找到所有父-子关系
    • 确定所有根节点
    • 根节点为起点,递归构建树

    代码实战

    找到所有父-子关系

    • 根据指定的idKeyparentKey
    • 当满足A[parentKey] === B[idKey]时,对象 A 是对象 B 的 child
    const findParentChildren = (items, idKey, parentKey) => {
      let parnetWrapper = new Map();
      items.forEach((item) => {
        if (parnetWrapper.has(item[parentKey])) {
          let children = parnetWrapper.get(item[parentKey]);
          children.push(item);
        } else {
          parnetWrapper.set(item[parentKey], [item]);
        }
      });
      return parnetWrapper;
    };
    

    在上面例子中

    const idKey = "id";
    const parentKey = "parent";
    const parnetWrapper = findParentChildren(data);
    

    确定所有根节点

    根节点的确定可以分两种情况讨论:

    • roolVal 已知,根据parentKey === rootVal可以确定哪些节点是根节点
    • roolVal 未知,只要parentKey 不在任意 idKey中,该节点就是根节点
    const findRoots = (items) => {
      if (rootVal) {
        return items.filter((ele) => ele[parentKey] === rootVal);
      } else {
        return items.filter(
          (parent) => !items.find((item) => item[idKey] === parent[parentKey])
        );
      }
    };
    

    在上面例子中,我们同样可以用两种方式确定:

    • 已知rootVal = "root"
    • 无法找到id === "root"的数据,因此id === "node-1" || "node-2"的节点是根节点
    // const rootVal = "root";
    const roots = findRoots(data);
    

    递归构建树

    const buildTree = (roots, wrapper) => {
      return roots.map((item) => {
        if (wrapper.has(item[idKey])) {
          return {
            ...item,
            children: buildTree(wrapper.get(item[idKey]), wrapper),
          };
        } else {
          return item;
        }
      });
    };
    

    TypeScript 实现版本见 buildTree

    树形结构转扁平化数组

    在组件开发中,扁平化数组也是很常见的操作。比如,级联组件一开始传入一个树形结构,为了方便后面的过滤和搜索,我们会先对树形结构进行扁平化处理。这个过程其实就是数组遍历的过程,递归或者迭代都可以进行处理。

    const flattenTree = (
      items,
      childrenKey = "children"
    ) => {
      const flattenOptions = [];
      dfs(items);
      return flattenOptions;
      function dfs(nodes) {
        if (nodes == null) {
          return;
        }
        for (const node of nodes) {
          if (!node[childrenKey] || !node[childrenKey].length) {
            flattenOptions.push(node);
          } else {
            dfs(node[childrenKey]);
          }
        }
      }
    };
    

    TypeScript 实现版本见 flattenTree

    参考源码


    起源地下载网 » JS树形结构处理 | 七日打卡

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元