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

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

相关推荐
知识分享小能手35 分钟前
CSS3学习教程,从入门到精通,CSS3 浮动与清除浮动语法知识点及案例代码(14)
前端·css·后端·学习·html·css3·html5
bin915336 分钟前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加导出数据功能示例9,TableView15_09带排序的导出表格示例
开发语言·前端·javascript·vue.js·ecmascript·deepseek
KjPrime4 小时前
纯vue手写流程组件
前端·javascript·vue.js
码农不惑5 小时前
前端开发:Vue以及Vue的路由
前端·javascript·vue.js
烛阴7 小时前
JavaScript instanceof:你真的懂它吗?
前端·javascript
shadouqi8 小时前
1.angular介绍
前端·javascript·angular.js
痴心阿文8 小时前
React如何导入md5,把密码password进行md5加密
前端·javascript·react.js
hdk19938 小时前
Edge浏览器登录微软账户报错0x80190001的解决办法
前端·microsoft·edge
徐同保9 小时前
yarn 装包时 package里包含[email protected]报错
前端·javascript