前言
常做B端的同学应该经常和 Table 组件打交道, 我之前写管理后台的时候, 基本上就是 antd 的 Table 组件一把梭, 也从未关心过内部实现.
其实社区有很多非常优秀的 Table 组件, 例如这篇文章将要分析的 TanStack Table 组件, 起因是这个库的抽象程度很高, 且不附带任何样式, 核心代码与框架无关, 因此只需要关注最纯粹的 Table 逻辑实现, 硬啃一遍后对抽象和逻辑能力, 甚至对ts的理解运用都有一定提高, 因此分享出来
概览
TanStack Table是一个无头表库, 意味着只有逻辑实现, 不附带样式, 本身和前端框架无关, 支持在各个框架中使用(React, Vue, Angular, svelte, solid等等)
utils工具函数
ts 工具类型
我们第一步先从简单的ts编写的工具类型开始(其实也不简单), 不过开卷有益
ts
type PartialKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
type RequiredKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
起手就是有点秀的ts类型体操, 其实耐心分析也比较好理解, Pick, Partial, Omit, Required都是 ts 内置的工具类型, 把这些组合起来实现需要的功能, 挨个说明
Pick用于从接口中提取一些类型
ts
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Obj = {
name: string;
age: 18;
gender: "male" | "female";
}
type NameAndAge = Pick<Obj, 'name' | 'age'>
// 取出的类型接口为
type NameAndAge = {
name: string;
age: 18;
}
Omit用于取出没有被选中的那部分类型, 可以理解为挑中的属性不要, 要没挑中的
和Pick相反
ts
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Obj = {
name: string;
age: 18;
gender: "male" | "female";
}
type OnlyGender = Omit<Obj, 'name' | 'age'>
// 挑中的name和age属性不要, 取出剩下的
type OnlyGender = {
gender: "male" | "female";
}
Partial用于将类型中所有属性变为可选
ts
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Obj = {
name: string;
age: 18;
gender: "male" | "female";
}
type PartialObj = Partial<Obj>
// 取出的类型接口为
type PartialObj = {
name?: string | undefined;
age?: 18 | undefined;
gender?: "male" | "female" | undefined;
}
Required和Partial作用相反, 用于将类型中所有属性变为必选
我们再来看开头这两个工具类型
ts
type PartialKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
// 第一部分Omit<T, K>表示取出T中不是K的属性
// 第二部分Partial<Pick<T, K>>, 表示先从T中取出一些属性K, 然后将这些属性设置为可选
// 两部分合在一起表示将类型中的一些属性key设置为可选, 另一些不变
type RequiredKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
// 同理分析可得, 两部分合在一起表示将类型中的一些属性key设置为必选, 另一些不变
进阶难度
ts
export type Overwrite<T, U extends { [TKey in keyof T]?: any }> = Omit<T,keyof U> & U
Overwrite接受两个参数类型, 其中第二个参数U的限制是, 必须继承于T, 这个继承直白点说就是U的属性只能和T一致(可以少不能多), 属性对应的值可以不一样,这是理解这个工具类型的关键 然后从T中取出了不是U的那部分, 再和U本身交叉, 就是用U中的和T值不一样的类型来改写T
看代码:
ts
type ObjA = {
name: string;
age: string;
}
type ObjB = {
age: number; // 和ObjA的age不一样
}
type Res = Overwrite<ObjA, ObjB>
// ObjB的age类型覆盖了ObjA的
type Res = {
name : string,
age: number
}
更进一步
在分析之前想到了一个面试题, 如果让你设计一个组件, 接收时,分,秒三个参数显示时间, 分钟和秒必须是数字0-59, 小时必须是数字0-24, 肯定不能直接用number类型, 如何设计?
首先ts无法表示某一个具体的数字, 但是可以访问到数组的length, 用数组长度表示数字
ts
type Arr = [1,2,3]
type get3 = Arr['length'] // type get3 = 3
看源码
ts
type ComputeRange<
N extends number,
Result extends Array<unknown> = [],
> = Result['length'] extends N
? Result
: ComputeRange<N, [...Result, Result['length']]>
ComputeRange就接收一个数字, 和一个空数组, 这里还用到了递归, 相当于不断往数组中插入值(数组的长度), 直到数组的长度达到数字N
这段话比较抽象, 翻译成js代码先
js
const fun = (n, arr = []) => {
if(arr.length === n){
return arr
}
arr.push(arr.length)
fun(n, arr)
return arr
}
console.log(fun(5));
// [0, 1, 2, 3, 4]
ts将数组转换为联合类型只需要访问数组的number属性
ts
type Arr = [1,2,3]
type Res = Arr[number]
// 取出的类型为
type Res = 1 | 2 | 3
回到上面的面试题, 假设秒数的数值范围是0-59, 可以这么写
ts
type ComputeRange<
N extends number,
Result extends Array<unknown> = [],
> = Result['length'] extends N
? Result
: ComputeRange<N, [...Result, Result['length']]>
type Seconds = ComputeRange<60>[number]
type Seconds = 0 | 1 | 2 | 3 ... | 59
工具函数
flat
不多说, 一道常见面试题: 手写flat, 看源码果然是有好处的, 分析都写在注释了
ts
function flattenBy<TNode>(
arr: TNode[],
getChildren: (item: TNode) => TNode[]
) {
// 创建结果数组
const flat: TNode[] = []
// 执行函数
const recurse = (subArr: TNode[]) => {
subArr.forEach(item => {
// 将结果存入结果数组
flat.push(item)
const children = getChildren(item)
if (children?.length) {
// 关键在这步, 如果子元素还是数组, 递归拍平
// 并将拍平的结果存入一开始的结果数组
recurse(children)
}
})
}
// 开始执行
recurse(arr)
return flat
}
memo
所谓的memo, 简单来说就是依赖发生变化就会重新执行函数, 更新结果, 否则就直接返回原来的结果(有点像缓存), 这也有可能是一个面试题哟~
源码删掉了一些debug的东西, 分析写在注释
ts
function memo<TDeps extends readonly any[], TDepArgs, TResult>(
getDeps: (depArgs?: TDepArgs) => [...TDeps],
fn: (...args: NoInfer<[...TDeps]>) => TResult,
opts: {
key: any
onChange?: (result: TResult) => void
}
): (depArgs?: TDepArgs) => TResult {
// 一开始的依赖数组
let deps: any[] = []
// 闭包变量 用于缓存结果
let result: TResult | undefined
// 返回一个新函数, 参数是依赖数组
return depArgs => {
const newDeps = getDeps(depArgs)
// 判断依赖有无变化, 主要是长度是否一致, 遍历依赖数组是否全等
const depsChanged =
newDeps.length !== deps.length ||
newDeps.some((dep: any, index: number) => deps[index] !== dep)
// 如果依赖没有变化, 那么直接返回缓存的结果
if (!depsChanged) {
return result!
}
// 依赖有变化, 跟新依赖数组
deps = newDeps
// 重新计算结果
result = fn(...newDeps)
// 有回调执行回调
opts?.onChange?.(result)
// 返回计算的结果
return result!
}
}
第一篇先写到这里, 后面持续更新更多