实现element ui中el-tree组件在lazy模式下的刷新

前言

最近我在研究把我们公司内部的产品重构,需要做一个树型组件,类似像下面这样。

树组件效果图

功能需求是:当在输入框内输入关键字搜索时,从服务端获取匹配的树节点数据,重新加载到树型组件当中。

已知的可用于刷新树节点的办法

tree组件提供了几种方式来加载树节点:

  • 提供data属性,tree组件会按照data的数据结构渲染树节点
  • 使用updateKeyChildren方法
  • 使用append方法
  • 提供lazy属性和load属性,当树组件lazy属性为true时,会使用load指定的方法渲染树节点

以上方式,都有可能用来实现我们开篇提出的需求。但是前三种方法基本被pass,因为前三种方法都是偏向本地数据渲染,而我们的数据都是lazy加载的。

lazy加载(懒加载)的意思是:每次只加载当前层级或者当前节点的子节点的数据,更深层级的数据需要主动触发后再加载,比如:点击展开图标等,这样能提升性能。

本地数据渲染有什么问题

我们在做一个树组件时,不得不考虑的一个问题是:树展开图标,也就是开篇图中每个item前面的三角形符号,点击该符号会加载当前节点的子节点。

tree组件提供了一个属性isLeaf来代表当前是否叶子节点,但只在lazy模式下生效。也就是说,如果我们通过data属性渲染树节点,就无法通过isLeaf属性指定叶子节点,tree组件会自动根据数据的children属性的值来决定是否叶子节点。

在当前场景中,我们的数据都是远程加载,由于懒加载机制,我们无法提前获得子节点数据,树展开图标难以加载,所以暂时不考虑通过data属性来处理我们的需求。

通过懒加载方式处理

如我们之前第四点办法所说:

提供lazy属性和load属性,当树组件lazy属性为true时,会使用load指定的方法渲染树节点

load属性的值是一个函数,该函数会在首次加载树以及手动触发加载子节点时调用。该函数接收两个参数:

  • node:是当前需要加载子节点的节点对象,如果没有提供有效的节点对象,则表示根节点
  • resolve:是一个函数,该函数接收一个数组参数,该函数负责把参数中的数据渲染到node节点之下

这个函数提供给树组件之后,会由树组件自动调用。

有的同学可能已经看出,load函数可能就是契机。

那么问题来了:

如何手动调用load函数

load函数其实很好定义,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
fLoadKeywords(node: Object, resolve: Function, searchStr?: string) {
    let params = {};
    if (node.id) {
      params.id = node.id;
    }
    if (search_str) {
      params.name = searchStr;
    }
    this.fKeywordsAction(params).then(data => {
      const transData = transforms.transformKeywords(data.content, node.id);
      resolve(transData);
    });
}

而我们监听搜索框输入时,手动调用load函数。监听搜索输入函数如下:

1
2
3
4
5
6
fFilterKeywords(val: string) {
    // todo 清除根节点
    
    // 主动调用load函数,但是这里有一个问题,如何提供resolve参数???
    this.fLoadKeywords([], resolve, val);
}

在这一步,我们遇到了两个问题:

  1. 我们需要根节点数据以便清除根节点
  2. 我们需要知道resolve函数

那我们如何得到resolve函数?聪明的同学可能已经想到了,我们在load函数第一次被tree组件调用时,将resolve和根节点数据暂存起来。我们将load函数修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fLoadKeywords(node: Object, resolve: Function, searchStr?: string) {
    // 将resolve函数存储在别的地方,在搜索的时候会使用
    if (!this.tree.resolve) {
      this.tree.resolve = resolve;
    }
    let params = {};
    if (node.data && node.data.id) {
      params.id = node.data.id;
    }
    if (search_str) {
      params.name = searchStr;
    }
    this.fKeywordsAction(params).then(data => {
      // console.log(data);
      const trans_data = transforms.transformKeywords(data.content, node.id);
      
      // 将根节点数据暂存起来
      if (node.id === 0 || node.length === 0) {
        this.tree.dataLevel1 = transData;
      }
      resolve(trans_data);
    });
}

再将监听搜索框函数修改一下:

1
2
3
4
5
6
7
8
9
10
11
fFilterKeywords(val: string) {
    // 清除节点
    const tree = this.$refs.treeKeywords.$refs.tree;
    if (this.tree.dataLevel1 && this.tree.dataLevel1.length) {
      this.tree.dataLevel1.forEach(d => {
        tree.remove(d);
      });
    }
    
    this.fLoadKeywords([], this.tree.resolve, val);
}

结束语

总体来说,比较奇技淫巧。

但我觉得问题的根源是,树展开图标lazy模式之间的矛盾:如果在非lazy模式下允许通过isLeaf属性指定叶子节点,那这个问题就不再是问题,我们可以通过非lazy模式来解决当前的问题。

所以我觉得树组件更好的设计是:

isLeaf: 指定节点是否为叶子节点,与lazy无关。在lazy = false的情况下,如果没有显式指定isLeaf属性, 则根据children属性的值判断是否为叶子节点。