记一次被 el-table 树形懒加载坑惨了的填坑实录(Vue3 + Element Plus)

184 阅读3分钟

背景:产品经理的一个“小需求”

那天下午,产品经理端着枸杞茶晃悠到我工位:

"咱们那个任务管理系统啊,父任务下要支持无限层级子任务,用户说操作完经常要手动刷新才能看到变化..."

我表面笑嘻嘻点开 Element Plus 文档,内心 OS:不就是个树形表格懒加载吗?洒洒水啦~

转存失败,建议直接上传图片文件


第一回合:天真的我 vs 倔强的 el-table

随手撸出标准写法:

<el-table
  :data="tableData"
  lazy
  :load="loadChild"
  row-key="taskId"
  ref="tableRef"
>
  <!-- 列定义 -->
</el-table>

接口对接完毕,增删改查一气呵成!

但很快,噩梦来了:

  • 新增子项:父节点秒收合再展开(用户:这动画晃得我头晕)
  • 删除子项:隔壁兄弟节点突然自闭(节点:我当时害怕极了)
  • 修改数据:看心情决定是否刷新(我:你礼貌吗?)

第二回合:与 Element Plus 源码的激情对线

翻源码的时候,我发现 lazyTreeNodeMap 这个内部状态对象掌控着懒加载节点的生杀大权,而 Element Plus 并没有给我们暴露一个合适的 API 来精细化管理它。

问题来了:

  • 每次数据变更后,el-table 只会“记住”旧数据,不会主动重新加载子节点!
  • 直接修改 lazyTreeNodeMap 会导致展开状态丢失,甚至触发表格异常!

这时候,我意识到得自己动手封装一个合理的缓存管理方案。


第三回合:科学疗法——自研懒加载管理方案

1. 设计一个缓存管理器

首先,我们用 Map 来存储 el-table 懒加载的节点信息,以便后续操作:

const loadNodeMap = new Map<string, { row: any; treeNode: any; resolve: Function }>();

2. 统一的父节点刷新方法

当子节点数据发生变化时,我们需要精准更新对应的 lazyTreeNodeMap,而不是暴力刷新整个表格。

const refreshParentNode = async (parentId: string) => {
  const parentLoadInfo = loadNodeMap.get(parentId);
  if (!parentLoadInfo) return;

  const { row, treeNode, resolve } = parentLoadInfo;

  // 触发缓存更新
  if (tableRef.value?.store?.states.lazyTreeNodeMap?.value) {
    tableRef.value.store.states.lazyTreeNodeMap.value[parentId][0]['loadState'] = '';
  }

  // 重新加载数据
  await loadChild(row, treeNode, resolve);
};

思路解析:

  1. 先取缓存,确保 parentId 存在
  2. 手动触发懒加载缓存更新,避免 el-table 继续使用旧数据
  3. 重新加载子节点,让 el-table 正确渲染最新数据

3. 实现 loadChild 方法

const loadChild = async (row: any, treeNode: any, resolve: Function) => {
  try {
    const parentId = row.taskId; // 以 taskId 作为 parentId
    const res = await taskChildListApi({ projectId: props.projectId, parentId });

    if (res.code === 200) {
      const children = reactive(res.rows);
      resolve(children || []);
      row.children = children;
      loadNodeMap.set(row.taskId, { row, treeNode, resolve });
    } else {
      resolve([]);
      row.children = [];
    }
  } catch (error) {
    PageUtil.msgError('加载子任务列表出错');
    resolve([]);
  }
};

这里的关键点:

  • 接口调用后,直接 resolve(children)el-table 识别新数据
  • 存入 loadNodeMap,方便后续精准更新

终极优化:防止频繁触发刷新

由于用户操作频繁,我们可以给 refreshParentNode 加一个 debounce,防止高频调用:

import { debounce } from 'lodash';
const safeRefresh = debounce(refreshParentNode, 300);

这样一来,即使用户疯狂点击,系统也不会频繁触发 API 请求,性能大大提升!


血泪总结:Element Plus 树表生存指南

  1. 懒加载缓存三原则

    • 改缓存前先拍照(存展开状态)
    • 动刀只切病变部位(精准更新 lazyTreeNodeMap
    • 完事儿记得恢复现场nextTick 大法)
  2. 性能优化骚操作

    // 给频繁操作上减速带
    import { debounce } from 'lodash';
    const safeRefresh = debounce(refreshParentNode, 300);
    
  3. 兜底方案

    // 被逼急了的终极奥义(慎用!)
    tableRef.value?.doLayout(); // 强制重绘
    

后记

当我把这个方案提交后,产品经理看着丝般顺滑的更新效果,终于放下了他 40 米长的需求大刀。而我在深夜的办公室,默默删掉了准备提交的离职申请...