前言
在上一篇文章 TanStack Table 一个优秀的开源表格组件源码分析(一) 中简单介绍了这个项目中的几个重要的工具类型和工具函数(flat和手写memo)作为入门, 这一篇会分析核心架构.
核心元素
在tanstack table, 整个表格分为了这些核心概念
- 行 rows
- 列 columns
- 单元格 cells
- 表头 headers
- 表格 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
理解了这些, 就对核心概念理解的差不多了, 剩下的只是具体实现而已, 在第三篇源码分析中会有更多介绍.