Ant Design TreeSelect 树选择控件“异步加载” - zptime/blog GitHub Wiki

之前对Ant Design TreeSelect 树选择控件进行了基础封装,这一篇主要讲异步数据的处理,也顺便讲了一下深度优先遍历(DFS)和广度优先遍历(BFS)。

封装原因:之前部门树数据是一次性加载的, 存在着性能问题,数据量很大的时候,加载很缓慢,而且组件可能崩溃;因此改成异步加载数据,系统中用到的地方很多,不可能每个地方都改一下,进行封装统一处理。

Ant Design TreeSelect 树选择控件二次封装及原理:https://juejin.cn/post/7003280618473668639

组件封装

  1. departSource:数据源,默认展示根部门和一级部门数据
  2. onLoadData:异步加载数据,根据部门 id 查询直接子部门数据,加载到数据源中
  3. treeExpandedKeys:展开的树节点,默认展开根部门
<template>
  <a-tree-select
    v-bind="$attrs"
    v-on="selectListeners"
    class="m-width"
    placeholder="请选择"
    searchPlaceholder="请输入部门名称"
    :treeDefaultExpandAll="false"
    :treeData="departSource"
    :load-data="onLoadData"
    @treeExpand="handleExpand"
    :treeExpandedKeys="treeExpandedKeys"
    :dropdownStyle="{ maxHeight: '400px' }"
    show-search
    treeNodeFilterProp="title"
  >
  </a-tree-select>
</template>

模拟数据

模拟数据

const tree = [
  {
    id: 1,
    name: "根部门",
    departNumber: 6,
    children: [
      {
        id: 11,
        pid: 1,
        name: "一级部门1",
        departNumber: 2,
        children: [
          { id: 111, pid: 11, name: "二级部门1", departNumber: 0 },
          { id: 112, pid: 11, name: "二级部门2", departNumber: 0 },
        ],
      },
      { id: 21, pid: 1, name: "一级部门2", departNumber: 0 },
      { id: 31, pid: 1, name: "一级部门3", departNumber: 0 },
      { id: 41, pid: 1, name: "一级部门4", departNumber: 0 },
      { id: 51, pid: 1, name: "一级部门5", departNumber: 0 },
      {
        id: 61,
        pid: 1,
        name: "一级部门6",
        departNumber: 2,
        children: [
          { id: 611, pid: 61, name: "二级部门3", departNumber: 0 },
          { id: 612, pid: 61, name: "二级部门4", departNumber: 0 },
        ],
      },
    ],
  },
];

深度优先遍历(DFS)

深度优先遍历(DFS -- Depth First Search):从根节点出发,先访问一个完整的子树,再访问相邻未被访问的子树,直到所有子树都被访问完。访问子树的操作和之前一样,直到所有节点都被访问到为止。

简单的图形化展示:

图示展示

函数代码展示:

/**
 *  深度优先遍历
 *  @params {Array} tree 树数据
 *  @params {Array} func 操作函数
 */
const dfsTransFn = (tree, func) => {
  tree.forEach((node) => {
    func(node);
    // 如果子树存在,递归调用
    if (node.children && node.children.length) {
      dfsTransFn(node.children, func);
    }
  });
};

// 示例1:打印节点
dfsTransFn(tree, (node) => {
  console.log(`${node.id}...${node.name}`);
});

// 示例2:树转化为列表
let treeList = [];
dfsTransFn(tree, (node) => {
  treeList.push(node);
});
console.log(treeList);

函数运行效果展示:

代码效果展示

广度优先遍历(BFS)

广度优先遍历(BFS -- Breadth First Search):从根节点出发,先访问根节点所在的初始队列,进行操作,并将该节点的所有子节点加入队列中;再依次访问队列中的第一个元素,进行操作,直到队列为空。即访问树结构的第 n+1 层前必须先访问完第 n 层,彻底搜索整个树结构,直到找到结果为止。

简单的图形化展示:

图示展示

函数代码展示:

/**
 *  广度优先遍历
 *  @params {Array} tree 树数据
 *  @params {Array} func 操作函数
 */
const bfsTransFn = (tree, func) => {
  let node,
    list = [...tree];
  // shift()-取第一个;pop()-取最后一个
  while ((node = list.shift())) {
    func(node);
    // 如果子树存在,递归调用
    node.children && list.push(...node.children);
  }
};

// 示例1:打印节点
bfsTransFn(tree, (node) => {
  console.log(`${node.id}...${node.name}`);
});
// 示例2:树转化为列表
let treeList = [];
bfsTransFn(tree, (node) => {
  treeList.push(node);
});
console.log(treeList);

函数运行效果展示:

代码效果展示

两者的区别

  1. 深度优先:不需要记住所有节点,占用空间小;堆栈形式,先进后出
  2. 广度优先:需要记录所有节点,占用空间大;队列形式,先进先出

真实数据

初始化处理

初始化时,需要请求根部门数据和一级部门数据

  1. queryRootDepartDetail:该接口用于获取根部门详情数据;
  2. queryChildDeparts:该接口通过部门 id 获取直接子部门数据;
  3. isLeaf:是否是叶子节点,通过是否存在子部门(o.departNumber>0)判断
<script>
  import * as R from "ramda";
  // 转化函数:将接口字段转换成组件需要的数据
  // 主要是添加isLeaf参数
  const tranFn = (data) => {
    if (R.type(data) === "Array" && R.length(data) > 0) {
      return R.map(
        (o) => ({
          value: o.id,
          key: o.id,
          title: o.name,
          children: [],
          // 是否是叶子节点:通过是否存在子部门判断
          isLeaf: !o.departNumber,
        }),
        data
      );
    } else {
      return [];
    }
  };

  export default {
    created() {
      this.queryAsyncDeparts();
    },
    methods: {
      async queryAsyncDeparts() {
        let res = await queryRootDepartDetail();

        // 请求根部门数据
        let departId = R.path(["data", "id"], res);
        // 请求根部门数据
        this.departSource = tranFn([res.data]);
        // 默认展开根节点
        this.treeExpandedKeys = [departId];
        // 通过根部门ID加载子部门数据,初始化展示根部门和一级部门数据
        this.queryMoreData(departId);
      },
      queryMoreData(departId) {},
    },
  };
</script>

实现效果展示:

截图

异步加载树节点

要在对应的部门树节点添加数据,有两种方式:深度优先遍历和广度优先遍历

<script>
  /**
   *  异步加载数据(深度优先遍历)
   *  @params {Array} tree 树数据
   *  @params {number} departId 部门id
   *  @params {Array} childList 加载的数据
   */
  const transTreeFn = (tree, departId, childList) => {
    return R.map((o) => {
      if (o.key === departId) {
        o.children = tranFn(childList);
      } else if (o.children && o.children.length) {
        transTreeFn(o.children, departId, childList);
      }
      return o;
    }, tree);
  };

  export default {
    methods: {
      // “加载更多”事件
      onLoadData(treeNode) {
        const { key } = treeNode.dataRef;
        this.queryMoreData(key);
      },
      // 异步数据加载
      queryMoreData(departId) {
        // 通过部门ID,在数据源中找到对应的数据,填充子部门数据
        queryChildDeparts(departId).then((res) => {
          // 模拟数据
          // let childList = [
          //   { id: 1001, name: "添加部门1", departNumber: 0 },
          //   { id: 1002, name: "添加部门2", departNumber: 0 },
          // ];

          let childList = R.pathOr([], ["data"], res);
          this.departSource = transTreeFn(
            this.departSource,
            departId,
            childList
          );
        });
      },
      // 控制展开节点
      handleExpand(expandedKeys) {
        this.treeExpandedKeys = expandedKeys;
      },
    },
  };
</script>

参考文档:

JS 树结构操作:查找、遍历、树结构和列表结构相互转换

JavaScript 实现 DOM 树的深度优先遍历和广度优先遍历

⚠️ **GitHub.com Fallback** ⚠️