需求简化

一个 Table,基于 antd。 需要提供大量默认render,如格式化、map、前后缀等;
而各个render间效果需要叠加

核心逻辑

基于用户故事,我采用链式渲染机制,流程如下。

graph LR START((渲染)) --> renderA renderA -->|next| renderB renderB -->|next| renderC renderC -->|next| renderD renderD --> END((返回标准 render))

如图,直观上有点像中间件,所以我们需要处理好渲染链和渲染节点的关系。
特别是后来居上的render,因为前者的渲染行为会被后者覆盖。

所以会有个问题,顺序重要,稍后我会通过代码来解释

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function _tsumugi<RecordType>(
  c: LightColumnProps<RecordType>
): TableColumnProps<RecordType>["render"] {
  const { basicRenderNode, ...node } = _toLightRenderNode(_factory(c));

  const chain = new RenderChain(basicRenderNode);
  const prepend = _pickNode(
      ["valueEnum", "valueType", "prefix", "suffix", "paragraph"],
      node
    ),
    append = _pickNode(["columnEmptyText"], node);

  chain
    .Prepend(basicRenderNode, ...prepend)
    ?.Append(basicRenderNode, ...append);

  return (v, r, i) => {
    let tmp = v;

    for (const n of chain) {
      tmp = n?.render?.(tmp, r, i, v);
    }

    return tmp;
  };
}

画成图是

graph TB START((开始)) --> valueEnum valueEnum -->|next| valueType valueType -->|next| prefix prefix -->|next| suffix suffix -->|next| paragraph paragraph -->|next| basic basic -->|next| columnEmptyText columnEmptyText --> END((结束))

我具体解释一下每个 render 的职责

render 职责 函数签名
valueEnum 值映射 select radio 用到 string => string
valueType 格式化, dateTime dateRange digit string => string
prefix 前缀 string => string
suffix 后缀 string => string
paragraph 文本处理,ellipsis copyable string => React.Node
basic 原点,手动渲染 any => React.Node
columnEmptyText 补 “-” any => React.Node | “-”

至此我们可以解释为什么顺序是重要的 —— 因为最后的 render 可以决定最终渲染行为。

所以我们需要一个有序的数据结构,并且可以考虑到将来,我们需要满足对各render顺序的各种(插入、交换、删除等)操作。

我们来看下 js 世界内置的数据结构

数据结构 有序 易用
Object
Array 中等
Set
Map

Object 无序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
console.log({
  a: "a",
  1: 1,
});
// { '1': 1, a: 'a' }

console.log({
  1: 1,
  a: "a",
});

// { '1': 1, a: 'a' }

set 和 map 的 weak 版本不参与讨论

看上去我们好像没得选,只能选数组了,虽然 js 的数组是对象、插入要用 splice、删除要用 splice(用 delete 导致不连续)、交换还是 splice。但是我知道,她是个好数据结构。

js 的数组是对象 数组

1
typeof [] === "object"; // true

delete 导致不连续

1
2
3
4
5
6
a = [1, 2, 3, 4, 5];

delete a[0];

console.log(a);
// [ <1 empty item>, 2, 3, 4, 5 ]

swap 3 5

1
2
3
4
a = [1, 2, 3, 4, 5];
a[a.indexOf(5)] = a.splice(a.indexOf(3), 1, a[a.indexOf(5)])[0];
console.log(a);
// [1, 2, 5, 4, 3]

乍一看封装一下也能用,且 js 动态数组是 hash-table 实现的,性能跟 chain 应该差不多。所以 chain 就没有优势了吗? 我们不妨换个角度思考问题:

  • 职责边界:数组太万能了,完全可以被滥用;
  • 维护成本:滥用引出的一个问题之一,一个通用的包应该是内聚且收敛的,不然迭代几个版本下来就变歪了。
  • 心智成本:同样是滥用引出的问题 —— 你不能因为一个物体有四条腿,然后是黄色的就说它是条狗吧。

综上所述,我决定把 chain 约束为链表,一来压缩了类型;二是降低职责。

额外思考
内存安全?
实现 reverse?