TanStack Table 一个优秀的开源表格组件源码分析(二)

前言

在上一篇文章 TanStack Table 一个优秀的开源表格组件源码分析(一) 中简单介绍了这个项目中的几个重要的工具类型和工具函数(flat和手写memo)作为入门, 这一篇会分析核心架构.

核心元素

在tanstack table, 整个表格分为了这些核心概念

  1. 行 rows
  2. 列 columns
  3. 单元格 cells
  4. 表头 headers
  5. 表格 table

核心概念的源码在/core目录下, 针对五个概念, 每个文件中都暴露一个函数: createXxx, 例如对于行来说, 返回createRow

js 复制代码
export const createRow = <TData extends RowData>(): Row<TData> => {
    // ...
}

以行为例的解析

源码频繁用到的一个ts泛型是TData extends RowData, 其实很好理解, 按照对表格的认识, 最重要的数据类型就是行的类型, 很简单的道理, 一行数据从左到右, 每格表达的数据都是不一样的, 类型自然也是不一的, 例如一行学生数据从左到右依次为: 学号, 名字, 年龄, 性别, 班级, 期末成绩, 选课数据, 兴趣爱好.

包括后面的表头 headers 本身就是对行的描述定义, 对于列来说, 每列数据的类型是一样的, 不一样的只是数据值

而整个表格的数据类型也由行类型确定下来, 从行相关代码入手开始看

行类型, 后面有详细解释

js 复制代码
export interface Row<TData extends RowData>
  extends CoreRow<TData>,
    VisibilityRow<TData>,
    ColumnPinningRow<TData>,
    RowPinningRow,
    ColumnFiltersRow<TData>,
    GroupingRow,
    RowSelectionRow,
    ExpandedRow {}

可以看到源码定义的接口继承了很多其他接口, 自己一行定义都没有, 真实开发中似乎很少这么做,都是一个interface一把梭了, 其实这是为了功能拆分, 解耦之后的类型更加清晰, 可扩展性更强, 把不同功能的类型定义分离开

举个例子: 一个人的类型, 有作为学生的属性和方法, 毕业后也有作为牛马的属性和方法

ts 复制代码
interface Person {
  name: string;
  age: number;
  写作业: () => {};
  上课: () => {};
  工资: number;
  上班: () => {};
}

例如我只想针对作为牛马的功能进行扩展, 扩展一个加班功能, 不影响核心属性和作为学生的功能和属性, 就需要先拆开这个类型接口

ts 复制代码
interface CoreState {
  name: string;
  age: number;
}

作为学生

ts 复制代码
interface Student {
  写作业: () => {};
  上课: () => {};
}

作为牛马, 我可以单独在这个里面新增一个加班的方法

ts 复制代码
interface NiuMa {
  工资: number;
  上班: () => {};
  // 新增
  加班: () => {};
}

最后定义人的类型继承这三者

ts 复制代码
interface Person extends CoreState, Student, NiuMa {}

回头看Row, 定义了泛型TData extends RowData, 同时本身由CoreRow和其他功能性的接口继承而来

CoreRow

ts 复制代码
export interface CoreRow<TData extends RowData> {
  id: string
  /**
   * 该行在其父数组(或根数据数组)中的索引。
   */
  index: number
  /**
   * 相对于根行数组的行深度(如果嵌套或​​分组)
   */
  depth: number
  /**
   * 返回一行所有单元格
   */
  getAllCells: () => Cell<TData, unknown>[]
  /**
   * 返回该行的叶行,不包括任何父行。
   */
  getLeafRows: () => Row<TData>[]
  /**
   * 如果存在,则返回该行的父行。
   */
  getParentRow: () => Row<TData> | undefined
  /**
   * 返回该行的父行,一直到根行。
   */
  getParentRows: () => Row<TData>[]
  /**
   * 返回给定 columnId 的行中值的唯一数组。
   */
  getUniqueValues: <TValue>(columnId: string) => TValue[]
  /** 返回给定 columnId 的行的值 */
  getValue: <TValue>(columnId: string) => TValue
  /**
   * 提供给表格的原始行对象。如果该行是分组行,则原始行对象将是组中的第一个原始行。
   */
  original: TData
  /**
   * 由 `options.getSubRows` 选项返回的原始子行数组。
   */
  originalSubRows?: TData[]
  /**
   * 如果嵌套,则为该行的父行 ID。
   */
  parentId?: string
  /**
   * 将给定 columnId 中的行的值呈现为与"getValue"相同,但如果未找到值,则返回"renderFallbackValue"。
   */
  renderValue: <TValue>(columnId: string) => TValue
  /**
   * 由 `options.getSubRows` 选项返回并创建的行的子行数组。
   */
  subRows: Row<TData>[]
  _getAllCellsByColumnId: () => Record<string, Cell<TData, unknown>>
  _uniqueValuesCache: Record<string, unknown>
  _valuesCache: Record<string, unknown>
}

有了这个类型定义, 就能顺理成章得写出row的数据结构, 整个createRow实际上, 主要也就做的这件事

ts 复制代码
export const createRow = <TData extends RowData>(
  table: Table<TData>,
  id: string,
  original: TData,
  rowIndex: number,
  depth: number,
  subRows?: Row<TData>[],
  parentId?: string
): Row<TData> => {
  let row: CoreRow<TData> = {
    id,
    index: rowIndex,
    original,
    depth,
    parentId,
    _valuesCache: {},
    _uniqueValuesCache: {},
    getValue: columnId => {},
    getUniqueValues: columnId => {},
    renderValue: columnId => {},
    subRows: subRows ?? [],
    getLeafRows: () => flattenBy(row.subRows, d => d.subRows),
    getParentRow: () =>
      row.parentId ? table.getRow(row.parentId, true) : undefined,
    getParentRows: () => {},
    getAllCells: memo(
      () => [table.getAllLeafColumns()],
      leafColumns => leafColumns.map(column =>
        createCell(table, row as Row<TData>, column, column.id)
      ),
    ),

    _getAllCellsByColumnId: memo(
      () => [row.getAllCells()],
      allCells => {},
    ),
  }

  for (const feature of table._features) {
    feature?.createRow?.(row as Row<TData>, table)
  }

  return row as Row<TData>
}

让我们先忽略掉里面方法的具体实现, 毕竟这一篇主要从项目架构入手, 定义row之后, 下面有一个for循环, 用features来遍历, 用其中的每个feature的createRow来改变初始的row对象

这个写法其实还给我不少启发, 这里的features数组是表格自己的, 意思是表格的功能, 表格的功能丰富多样, 从createRow的角度来看, 不关心功能是什么, 有什么影响, 只要每个feature有createRow这个方法, 就用它来"洗礼"一下初始的row对象, 初始row对象就变得更强了, 最后返回初始的row对象, row的类型是Row

仔细分析一下, 一开始定义row对象的时候, 是这样的, 只是个CoreRow类型

ts 复制代码
let row: CoreRow<TData> = {...}

但是最终返回的不是CoreRow而是Row, 再回过头看Row, 它继承了 CoreRow, VisibilityRow, RowPinningRow ... 这么多接口

js 复制代码
export interface Row<TData extends RowData>
  extends CoreRow<TData>,
    VisibilityRow<TData>,
    ColumnPinningRow<TData>,
    RowPinningRow,
    ColumnFiltersRow<TData>,
    GroupingRow,
    RowSelectionRow,
    ExpandedRow {}

正是由于features数组的遍历, 每个feature都来"洗礼"一下row对象, 让row拥有除了CoreRow之外的接口类型中的功能

比较容易理解的比喻

可能有人已经绕晕了, 我们在用最上面的人的例子来讲讲, 孩子从一出生什么功能都没有呢

初始化一个人, 就是一个普通的对象, 只有最简单的姓名, 性别和年龄属性

ts 复制代码
let people: CoreData = {
    name: '小帅',
    gender: '男',
    age: 1
}

但是这个人可能要经历各种各样的"事", 这些事都会让这个人变得更复杂, 更强大, 我们先不管这些事是什么, 都放在一个数组里面, 事情也很复杂, 怎么区分?

我们暂时分为对成绩有帮助的事, 和对成绩没帮助的事, 前两个对成绩有帮助, 逃课上网对成绩没帮助

js 复制代码
const features = [
    {
        desc: '上课认真听讲',
        "是否对成绩有帮助": true,
        "这件事怎么提高成绩的": () => {},
    },
    {
        desc: "参加课外补习班",
        "是否对成绩有帮助": true,
        "这件事怎么提高成绩的": () => {},
    },
    {
        desc: "逃课去网吧",
        "是否对成绩有帮助": false,
    },
    ...
]

for循环遍历这个数组, if(事情对成绩有帮助), 那自然这件事就有一个函数用于提高成绩, 执行回调, 用来改造人这个初始化对象

ts 复制代码
let people: CoreData = {
    name: '小帅',
    gender: '男',
    age: 1
}

for (const feature of table._features) {
    feature?.这件事怎么提高成绩的?.(people as Row<TData>)
}

return people as 被提高了成绩的人

有了这个比喻, 就可以明白feature也是一样的改造row对象, 由于feature很复杂, 我们只取feature中的createRow方法来改造row对象

最终呢, 这个人就变成了一个被提高了成绩的人, 同时也有了更多技能, 能匹配更丰富功能的接口类型

之所以这么大费周章解释行的构造, 是因为列, 单元格, 表头, 表格都是一样的结构

通用的解释

不管是行还是类, 还是其他的核心概念, 都是这样一个基础结构

js 复制代码
interface CoreData<TData> {
    ...
}

export createXxx = () => {
    let obj: CoreData<TData> = {
        ...
    }

    for (const f of table._features) {
        f.createXxx?.(obj)
    }

    return obj
}

先定义一个基础对象, 然后用features去洗礼这个对象, 让对象变得功能更丰富, 由于每个feature也很复杂, 所以只用对应的方法去改造

例如针对row, 就用feature?.createRow 针对column, 就用feature?.createColumn

理解了这些, 就对核心概念理解的差不多了, 剩下的只是具体实现而已, 在第三篇源码分析中会有更多介绍.

相关推荐
做前端的娜娜子1 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅1 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni2 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学3 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
赵庆明老师3 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim
wc884 小时前
微软EDGE浏览器功能学习
前端·学习·edge
Csvn4 小时前
🎯 原生 `<dialog>` 元素:终于可以扔掉一半的自定义弹窗组件了?
前端
Csvn4 小时前
🎨 CSS @layer:用「层叠优先级」终结样式打架的世纪难题
前端
濮水大叔5 小时前
不必把 Vue3 写成“麻花”:从状态碎片到对象协作,重新理解 Zova 的前端心智模型
前端·typescript·vue3·ioc·tsx·zova