element-plus el-table 树表格子节点更新bug 源码调试+提交PR

BUG描述

最近公司新开了一个项目,需要用到Vue3+ts,UI框架使用element-plus

说实话,vue3出来这么久,2023年我才用上实际项目。

项目上有这样一个场景,需要用到树表格懒加载,还要支持子节点的增、删、改

树表格子节点增、删、改
el-table树表格懒加载目前的实现方法是通过调用load,节点修改时,再次手动调用。
这是官网的例子:

<template>
  <div>
    <el-table
      :data="tableData1"
      style="width: 100%"
      row-key="id"
      border
      lazy
      :load="load"
      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
    >
      <el-table-column prop="date" label="Date" />
      <el-table-column prop="name" label="Name" />
      <el-table-column prop="address" label="Address" />
    </el-table>
  </div>
</template>
<script lang="ts" setup>
interface User {
  id: number
  date: string
  name: string
  address: string
  hasChildren?: boolean
  children?: User[]
}

const load = (
  row: User,
  treeNode: unknown,
  resolve: (date: User[]) => void
) => {
  setTimeout(() => {
    resolve([
      {
        id: 31,
        date: '2016-05-01',
        name: 'wangxiaohu',
        address: 'No. 189, Grove St, Los Angeles',
      },
      {
        id: 32,
        date: '2016-05-01',
        name: 'wangxiaohu',
        address: 'No. 189, Grove St, Los Angeles',
      },
    ])
  }, 1000)
}

const tableData1: User[] = [
  {
    id: 1,
    date: '2016-05-02',
    name: 'wangxiaohu',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    id: 2,
    date: '2016-05-04',
    name: 'wangxiaohu',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    id: 3,
    date: '2016-05-01',
    name: 'wangxiaohu',
    hasChildren: true,
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    id: 4,
    date: '2016-05-03',
    name: 'wangxiaohu',
    address: 'No. 189, Grove St, Los Angeles',
  },
]
</script>

里面并没有涉及这种场景,因此,在下面向百度编程得知,手动调用load的方式为:

// 先new一个map用于数据保存
const rowMaps = reactive(new Map())

// 加载子级时保存数据
const load = async (row: any, treeNode: unknown, resolve: (data: any[]) => void) => {
  let re = await getTreeChild({
    parentId: row.id
  })
  resolve(re.data)
  rowMaps.set(row.id, { row, treeNode, resolve, children: re.data })
}

// 刷新时
const refresh = (parentId: any) => {
  if (rowMaps.get(parentId)) {
    parentId = parentId ? parseInt(parentId) : 0
    // 获取相关数据并load
    const { row, treeNode, resolve } = rowMaps.get(parentId)
    load(row, treeNode, resolve)
  } else {
    getList()
  }
}

如此以来,增、删、改后动态更新子节点的问题就解决了,就当我愉快地准备进行下一项工作时,发现了一个不得了的bug

当这里只剩下一个子节点时,调用load就失效了。WTF
而我面向百度编程了好久,都没有得到解决。
这个新坑怕是被我踩到了。
于是去扒起了element-plus 的源码。

el-table 树表格load源码

首先,load可以执行,但是只剩一个子节点就有问题,那么就直接可以定位bug在load方法里:

文件路径:element-plus\packages\components\table\src\store\tree.ts

 const loadData = (row: T, key: string, treeNode) => {
    const { load } = instance.props as unknown as TableProps<T>
    if (load && !treeData.value[key].loaded) {
      treeData.value[key].loading = true
      load(row, treeNode, (data) => {
        if (!Array.isArray(data)) {
          throw new TypeError('[ElTable] data must be an array')
        }
        treeData.value[key].loading = false
        treeData.value[key].loaded = true
        treeData.value[key].expanded = true
        // 就是这里,我们的子节点删完了data.length == 0,无法赋值
        if (data.length) {
          lazyTreeNodeMap.value[key] = data
        }
        instance.emit('expand-change', row, true)
      })
    }
  }

目前的解决方法

PR已经提了,还在review,目前碰到的话用这种方式:

// 刷新时
const refresh = (parentId: any) => {
  if (rowMaps.get(parentId)) {
    parentId = parentId ? parseInt(parentId) : 0
    // 获取相关数据并load
    const { row, treeNode, resolve } = rowMaps.get(parentId)
    // 源码bug,需要先置空
    tableRef.value.store.states.lazyTreeNodeMap.value[parentId] = []
    load(row, treeNode, resolve)
  } else {
    getList()
  }
}

置空lazyTreeNodeMap.value中存的数据即可。

源码bug修复

既然找到问题所在,不如动动手解决一下,万一能merge,我也算是开源项目的贡献者之一了。这个成就,还是蛮不错的。哈哈。

我的想法是:

 // 源码bug,需要先置空
    tableRef.value.store.states.lazyTreeNodeMap.value[parentId] = []

这段语句实在是太难看了,这么长,最好是在源码里解决掉,外部通过一个方法调用一下,类似clear。

那么首先需要分析一下源码懒加载的实现方式。
源码里实现了一个类似store的类用于管理table的状态
其中tree.ts就是树表格的状态管理文件。

该文件中用updateTreeData方法实现数据更新。

const updateTreeData = (
    ifChangeExpandRowKeys = false,
    ifExpandAll = instance.store?.states.defaultExpandAll.value
  ) => {
    const nested = normalizedData.value
    const normalizedLazyNode_ = normalizedLazyNode.value
    const keys = Object.keys(nested)
    const newTreeData = {}
    if (keys.length) {
      const oldTreeData = unref(treeData)
      const rootLazyRowKeys = []
      const getExpanded = (oldValue, key) => {
        if (ifChangeExpandRowKeys) {
          if (expandRowKeys.value) {
            return ifExpandAll || expandRowKeys.value.includes(key)
          } else {
            return !!(ifExpandAll || oldValue?.expanded)
          }
        } else {
          const included =
            ifExpandAll ||
            (expandRowKeys.value && expandRowKeys.value.includes(key))
          return !!(oldValue?.expanded || included)
        }
      }
      // 合并 expanded 与 display,确保数据刷新后,状态不变
      keys.forEach((key) => {
        const oldValue = oldTreeData[key]
        const newValue = { ...nested[key] }
        newValue.expanded = getExpanded(oldValue, key)
        if (newValue.lazy) {
          const { loaded = false, loading = false } = oldValue || {}
          newValue.loaded = !!loaded
          newValue.loading = !!loading
          rootLazyRowKeys.push(key)
        }
        newTreeData[key] = newValue
      })
      // 根据懒加载数据更新 treeData
      const lazyKeys = Object.keys(normalizedLazyNode_)
      if (lazy.value && lazyKeys.length && rootLazyRowKeys.length) {
        lazyKeys.forEach((key) => {
          const oldValue = oldTreeData[key]
          const lazyNodeChildren = normalizedLazyNode_[key].children
          if (rootLazyRowKeys.includes(key)) {
            // 懒加载的 root 节点,更新一下原有的数据,原来的 children 一定是空数组
            if (newTreeData[key].children.length !== 0) {
              throw new Error('[ElTable]children must be an empty array.')
            }
            newTreeData[key].children = lazyNodeChildren
          } else {
            const { loaded = false, loading = false } = oldValue || {}
            newTreeData[key] = {
              lazy: true,
              loaded: !!loaded,
              loading: !!loading,
              expanded: getExpanded(oldValue, key),
              children: lazyNodeChildren,
              level: '',
            }
          }
        })
      }
    }
    treeData.value = newTreeData
    instance.store?.updateTableScrollY()
  }

可以看到数据以键值对的方式存储在变量newTreeData,通过newTreeData赋值treeData实现数据更新。

而该方法(updateTreeData)又监听了normalizedLazyNode

 watch(
    () => normalizedData.value,
    () => {
      updateTreeData()
    }
  )
  watch(
    () => normalizedLazyNode.value,
    () => {
      updateTreeData()
    }
  )

normalizedLazyNode就是lazyTreeNodeMap.value的计算属性

const normalizedLazyNode = computed(() => {
    const rowKey = watcherData.rowKey.value
    const keys = Object.keys(lazyTreeNodeMap.value)
    const res = {}
    if (!keys.length) return res
    keys.forEach((key) => {
      if (lazyTreeNodeMap.value[key].length) {
        const item = { children: [] }
        lazyTreeNodeMap.value[key].forEach((row) => {
          const currentRowKey = getRowIdentity(row, rowKey)
          item.children.push(currentRowKey)
          if (row[lazyColumnIdentifier.value] && !res[currentRowKey]) {
            res[currentRowKey] = { children: [] }
          }
        })
        res[key] = item
      }
    })
    return res
  })

因此,我们只需置空lazyTreeNodeMap.value的相关数据,即可触发更新。

于是,新增方法clearTreeNode

 // 子节点删除后手动调用更新
  const clearTreeNode = (key: string) => {
    if (treeData.value[key].loaded) {
      lazyTreeNodeMap.value[key] = []
    }
  }

现在方法写好了,我想通过

tableRef.value.clearTreeNode(parentId)
//(tableRef.value as any).clearTreeNode('3')

这种形式调用,看下如何实现。

el-table 文件:element-plus\packages\components\table\src\table.vue
一些公共方法定义在useUtils里
在这里插入图片描述
return 出去才能实现调用
再顺着这个思路往源头找,
element-plus\packages\components\table\src\table\utils-helper.ts ==>
element-plus\packages\components\table\src\store\index.ts ==>
element-plus\packages\components\table\src\store\watcher.ts ==>
element-plus\packages\components\table\src\store\tree.ts

这里比较绕,画了个图
文件调用链
所以,将写好的方法抛出,按照引用链传递,即可。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

element-plus 源码调试

接下来就是调试,看看有没有效果。

  1. pnpm i
    注意node需要16以上的
  2. npm run dev

调试的文件在:
element-plus\play\src\App.vue

<template>
  <div class="play-container">
    <el-button @click="refresh">refresh</el-button>
    <el-button @click="edit">edit</el-button>
    <el-table
      ref="tableRef"
      :data="tableData1"
      style="width: 100%"
      row-key="id"
      border
      lazy
      :load="load"
      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
    >
      <el-table-column prop="date" label="Date" />
      <el-table-column prop="name" label="Name" />
      <el-table-column prop="address" label="Address" />
    </el-table>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
interface User {
  id: number
  date: string
  name: string
  address: string
  hasChildren?: boolean
  children?: User[]
}
const tableRef = ref(null)
// 保存有子节点的父级
const rowMaps = reactive(new Map())
let arr1 = [
  {
    id: 31,
    date: '2016-05-01',
    name: 'wangxiaohu',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    id: 32,
    date: '2016-05-01',
    name: 'wangxiaohu',
    address: 'No. 189, Grove St, Los Angeles',
  },
]
const tableData1: User[] = [
  {
    id: 1,
    date: '2016-05-02',
    name: 'wangxiaohu',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    id: 2,
    date: '2016-05-04',
    name: 'wangxiaohu',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    id: 3,
    date: '2016-05-01',
    name: 'wangxiaohu',
    hasChildren: true,
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    id: 4,
    date: '2016-05-03',
    name: 'wangxiaohu',
    address: 'No. 189, Grove St, Los Angeles',
  },
]
// code here
const load = (
  row: User,
  treeNode: unknown,
  resolve: (date: User[]) => void
) => {
  setTimeout(() => {
    rowMaps.set(row.id, { row, treeNode, resolve, children: arr1 })
    if (arr1.length) {
      resolve(arr1)
    } else {
      ;(tableRef.value as any).clearTreeNode(3)
    }
  }, 1000)
}

const edit = () => {
  let parentId = 3
  arr1 = [
    {
      id: 32,
      date: '2016-05-01',
      name: '111111',
      address: 'No. 189, Grove St, Los Angeles',
    },
  ]
  const { row, treeNode, resolve } = rowMaps.get(parentId)
  load(row, treeNode, resolve)
}

const refresh = () => {
  let parentId = 3
  // 模仿最后一个子节点删除
  arr1 = []
  const { row, treeNode, resolve } = rowMaps.get(parentId)
  load(row, treeNode, resolve)
}
</script>

<style lang="scss">
html,
body {
  width: 100vw;
  height: 100vh;
  margin: 0;
  #play {
    height: 100%;
    width: 100%;
    .play-container {
      height: 100%;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    }
  }
}
</style>

在这里插入图片描述

经测试,完全满足要求。

如何提交PR

其实这也是我第一次向开源项目提交PR,要不是新坑,我也不会想到要提,也算是攒了个经验。
介绍下提交PR的步骤:

  1. fork源码仓库
    在这里插入图片描述
  2. clone 自己fork后的仓库
    在这里插入图片描述
  3. 在本地打开git 与fork仓库建立连接
git remote add upstream https://github.com/element-plus/element-plus.git
  1. 记得同步最新代码
git fetch upstream dev
  1. pull request 规范

  2. 提交代码

git add .
git commit -m "feat: (components):[el-table] feat clearTreeNode function"
git push origin dev
  1. 提交PR
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    等待review即可。

总结

以上就是我最近遇到问题以及解决问题的全过程,记录下。
解决的方法不一定好,但是,目前暂时想不到其他好的方式,对于element-plus的源码研究也就是个皮毛的程度,看能不能merge吧。

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值