TypeScript中应该禁止使用enum

TypeScript 添加了一个 enum 的数据类型结构,但是这个数据类型在 JavaScript 中并不存在,在编译过程中会被转换成 Object。并且 enum 类型可以完全被 union 类型替代。 所以我们推荐使用 union 来代替 enum。接下来我们使用一个例子来解释下 enum 如何被替换成 union。

# enum 定义数据类型

我们来封装一个常见的 fetch 函数,使用 enum 定义的代码如下。

enum HTTPRequestMethod {
  GET = 'GET',
  POST = 'POST'
}

function fetchJSON(url: string, method: HTTPRequestMethod) {
  return fetch(url, { method }).then(response => response.json());
}
1
2
3
4
5
6
7
8

编译成 JS 代码如下:

'use strict';
var HTTPRequestMethod;
(function(HTTPRequestMethod) {
  HTTPRequestMethod['GET'] = 'GET';
  HTTPRequestMethod['POST'] = 'POST';
})(HTTPRequestMethod || (HTTPRequestMethod = {}));
function fetchJSON(url, method) {
  return fetch(url, { method }).then(response => response.json());
}
1
2
3
4
5
6
7
8
9

这样编译的 JS 代码,定义了 HTTPRequestMethod 对象,但并不是 const 常量,有被更改的可能性,非常不优雅。

# union 定义数据类型

const HTTPRequestMethod = {
  GET: 'GET',
  POST: 'POST'
} as const;

type ValuesOf<T> = T[keyof T];
type HTTPRequestMethodType = ValuesOf<typeof HTTPRequestMethod>;

function fetchJSON(url: string, method: HTTPRequestMethodType) {
  return fetch(url, { method }).then(response => response.json());
}
1
2
3
4
5
6
7
8
9
10
11

编译成 js 代码之后是:

'use strict';
const HTTPRequestMethod = {
  GET: 'GET',
  POST: 'POST'
};
function fetchJSON(url, method) {
  return fetch(url, { method }).then(response => response.json());
}
1
2
3
4
5
6
7
8

# 总结

对比 enum 和 union 定义生成的代码,我们能明显感受到 union 类型生成的代码更优雅。而且 enum 还有其他缺点,大家可以查看下参考链接。

# 参考

さようなら、TypeScript enum (opens new window)

Why it is not good to use enums? (opens new window)

Const Assertions in Literal Expressions in TypeScript (opens new window)

JS中关于深拷贝和浅拷贝

JS 有基本数据类型和复合数据类型,基本数据类型包括 null,undefined,boolean,number,string,symbol。针对这些数据类型都是值拷贝。复合数据类型包括 object,以及衍生的内置数据类型,Array, Map, Set 等等。默认对复合数据类型的赋值操作都是引用拷贝,就是浅拷贝。

# JSON.stringify 实现深拷贝

一种常用的方式是使用 JSON 接口先转换成字符串再解析成对象。

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));
1

虽然 V8 引擎优化了 JSON 的解析速度,但是这种方式还是有一些限制条件的,如果数据中存在 Map, Date 等 JS 内置数据结构就没法正常复制。

# 手动实现 deepClone

export const isObject = (obj: any): boolean => {
  return obj !== null && typeof obj === 'object';
};

export const objectToString = Object.prototype.toString;
export const toTypeString = (value: unknown): string =>
  objectToString.call(value);

export const isMap = (val: unknown): val is Map<any, any> =>
  toTypeString(val) === '[object Map]';
export const isSet = (val: unknown): val is Set<any> =>
  toTypeString(val) === '[object Set]';
export const isDate = (val: unknown): val is Date =>
  toTypeString(val) === '[object Date]';
export const isRegExp = (val: unknown): val is RegExp =>
  toTypeString(val) === '[object RegExp]';

export function deepClone<T>(val: T): T {
  if (!isObject(val)) {
    return val;
  }
  if (Array.isArray(val)) {
    return val.map(deepClone) as typeof val;
  }
  if (isDate(val)) {
    return new Date(val) as typeof val;
  }
  if (isMap(val)) {
    return new Map(val) as typeof val;
  }
  if (isSet(val)) {
    return new Set(val) as typeof val;
  }
  if (isRegExp(val)) {
    return new RegExp(val.source, val.flags) as typeof val;
  }
  return Object.keys(val).reduce((acc, key) => {
    acc[key as keyof T] = deepClone(val[key as keyof T]);
    return acc;
  }, {} as T);
}
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

手动实现 deepClone 可以处理 Date, Map, Set, RegExp 这些数据结构,但这也不是完美的解决方案,例如二进制的数据结构 ArrayBuffer 就没在处理范围,但对于绝大多数场景也足够用了。

# 使用 structuredClone

const myDeepCopy = structuredClone(myOriginal);
1

最新的浏览器标准添加了 structuredClone 函数,可以实现常见数据结构的复制,但是有些数据结构也是不能复制的,例如 Function。不过 Function 也没有复制的必要。 由于这个函数最近才添加,实际使用中可能还需要搭配 polyfill (opens new window)

# 总结

实际开发工作中大家可以根据自己的需要选择合适的深拷贝解决方案。

# 参考

structuredClone (opens new window)

Deep-copying in JavaScript using structuredClone (opens new window)

cloneDeep (opens new window)

如何实现虚拟滚动条

最近在做UI组件,发现下拉组件经常会有很多选项导致渲染的DOM过多,在低性能的设备上交互变的很卡。例如最常见的需求就是下拉选择所在国家或者地区,全世界有两百多个国家或者地区。 如果把所有国家或者地区都渲染出来必然会导致DOM过多,在低性能设备上滚动起来都会很卡。这时候就需要使用虚拟滚动条来提高渲染性能。

# 虚拟滚动条原理

虚拟滚动的原理是显示的容器高度固定,我们只渲染用户看到的选项。但是选项容器的高度还是设置成所有选项的高度,这样滚动条效果保持一样,不过实际渲染的DOM数量却大大减少。

# 虚拟滚动条的数学计算

目前虚拟滚动的主要限制是需要知道显示容器的高度,渲染的每个选项的高度以及渲染的选项的数量。我们可以通过用户的滚动距离来计算需要显示的内容以及需要偏移的距离。

细节请参考下面的代码:

const VirtualScroll: React.FC<IVirtualScrollProps> = ({
  itemCount, // 选项的数量
  height, // 显示容器的高度
  childHeight, // 选项的高度
  itemData // 选项元素
}) => {
  const wrapperRef = useRef<HTMLDivElement>(null);
  // 得高容器滚动了多少距离
  const scrollTop = useScrollAware(wrapperRef);
  // 计算渲染的选项的总高度
  const totalHeight = useMemo(
    () => childHeight * itemCount,
    [childHeight, itemCount]
  );
  // 根据滚动距离,计算要渲染的选项的起始位置
  const startIndex = useMemo(
    () => Math.floor(scrollTop / childHeight),
    [scrollTop, childHeight]
  );
  // 根据容器的高度计算要渲染的选项的数量
  const visibleItemCount = useMemo(
    () => Math.ceil(height / childHeight),
    [height, childHeight]
  );
  // 根据渲染的选项的起始位置,计算选项的偏移量
  const offsetY = useMemo(
    () => startIndex * childHeight,
    [childHeight, startIndex]
  );
  // 计算实际显示的选项元素
  const visibleItemData = useMemo(() => {
    return itemData.slice(startIndex, startIndex + visibleItemCount);
  }, [itemData, startIndex, visibleItemCount]);

  return (
    <div
      style={{
        height,
        overflow: 'auto'
      }}
      ref={wrapperRef}
    >
      <div
        style={{
          position: 'relative',
          overflow: 'hidden',
          willChange: 'transform',
          height: totalHeight
        }}
      >
        <div
          style={{
            willChange: 'transform',
            transform: `translate3d(0, ${offsetY}px, 0)`
          }}
        >
          {visibleItemData}
        </div>
      </div>
    </div>
  );
};

export default memo(VirtualScroll);
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

完整的实现请参考 Simple UI - Select (opens new window)

# 总结

在知道虚拟滚动条的原理之后,发现并不难实现,重点要理解几个数值的计算,才能明白为什么这样能达到预期的效果。

# 参考

Build your Own Virtual Scroll (opens new window)