@taicode/common-base 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -0
- package/package.json +21 -3
- package/source/catch/catch.test.ts +341 -0
- package/source/catch/catch.ts +35 -0
- package/source/catch/index.ts +1 -0
- package/source/color/color.test.ts +144 -0
- package/source/{utils/color/index.ts → color/color.ts} +7 -0
- package/source/color/index.ts +1 -0
- package/source/debounce/debounce.test.ts +179 -0
- package/source/debounce/debounce.ts +42 -0
- package/source/debounce/index.ts +1 -0
- package/source/disposer/disposer.test.ts +257 -0
- package/source/disposer/index.ts +1 -0
- package/source/error/error.test.ts +293 -0
- package/source/error/error.ts +38 -0
- package/source/error/index.ts +1 -0
- package/source/event-emitter/event-emitter.test.ts +247 -0
- package/source/{utils/event-emitter/index.ts → event-emitter/event-emitter.ts} +2 -0
- package/source/event-emitter/index.ts +1 -0
- package/source/index.ts +19 -0
- package/source/logger/index.ts +43 -0
- package/source/logger/logger.test.ts +36 -0
- package/source/number/index.ts +1 -0
- package/source/number/number.test.ts +60 -0
- package/source/object/index.ts +1 -0
- package/source/{utils/object/index.test.ts → object/object.test.ts} +8 -8
- package/source/string/index.ts +1 -0
- package/source/string/string.test.ts +98 -0
- package/source/throttle/index.ts +1 -0
- package/source/throttle/throttle.test.ts +228 -0
- package/source/throttle/throttle.ts +76 -0
- package/test-exports.js +6 -0
- package/tsconfig.json +16 -21
- package/vitest.config.ts +8 -0
- package/source/decorators/debounce.ts +0 -25
- package/source/utils/poller/index.test.ts +0 -62
- package/source/utils/poller/index.ts +0 -57
- package/source/utils/promise/index.test.ts +0 -38
- package/source/utils/promise/index.ts +0 -45
- /package/source/{utils/disposer → disposer}/disposer.ts +0 -0
- /package/source/{utils/number/index.ts → number/number.ts} +0 -0
- /package/source/{utils/object/index.ts → object/object.ts} +0 -0
- /package/source/{utils/string/index.ts → string/string.ts} +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { bigintToNumber } from './number'
|
|
3
|
+
|
|
4
|
+
describe('bigintToNumber 函数', () => {
|
|
5
|
+
it('应该将小的 bigint 转换为 number', () => {
|
|
6
|
+
const result = bigintToNumber(123n)
|
|
7
|
+
expect(result).toBe(123)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('应该将负数 bigint 转换为 number', () => {
|
|
11
|
+
const result = bigintToNumber(-456n)
|
|
12
|
+
expect(result).toBe(-456)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('应该将零 bigint 转换为 number', () => {
|
|
16
|
+
const result = bigintToNumber(0n)
|
|
17
|
+
expect(result).toBe(0)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('应该为 null 输入返回 0', () => {
|
|
21
|
+
const result = bigintToNumber(null)
|
|
22
|
+
expect(result).toBe(0)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('应该处理 MAX_SAFE_INTEGER 的 bigint', () => {
|
|
26
|
+
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER)
|
|
27
|
+
const result = bigintToNumber(maxSafe)
|
|
28
|
+
expect(result).toBe(Number.MAX_SAFE_INTEGER)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('应该处理 MIN_SAFE_INTEGER 的 bigint', () => {
|
|
32
|
+
const minSafe = BigInt(Number.MIN_SAFE_INTEGER)
|
|
33
|
+
const result = bigintToNumber(minSafe)
|
|
34
|
+
expect(result).toBe(Number.MIN_SAFE_INTEGER)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('应该为 bigint 溢出(正数)返回 MAX_SAFE_INTEGER', () => {
|
|
38
|
+
const tooBig = BigInt(Number.MAX_SAFE_INTEGER) + 1n
|
|
39
|
+
const result = bigintToNumber(tooBig)
|
|
40
|
+
expect(result).toBe(Number.MAX_SAFE_INTEGER)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('应该为 bigint 溢出(负数)返回 MAX_SAFE_INTEGER', () => {
|
|
44
|
+
const tooSmall = BigInt(Number.MIN_SAFE_INTEGER) - 1n
|
|
45
|
+
const result = bigintToNumber(tooSmall)
|
|
46
|
+
expect(result).toBe(Number.MAX_SAFE_INTEGER)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('应该处理非常大的 bigint', () => {
|
|
50
|
+
const veryLarge = 999999999999999999999n
|
|
51
|
+
const result = bigintToNumber(veryLarge)
|
|
52
|
+
expect(result).toBe(Number.MAX_SAFE_INTEGER)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('应该处理从 number 创建的 bigint', () => {
|
|
56
|
+
const bigintFromNumber = BigInt(42)
|
|
57
|
+
const result = bigintToNumber(bigintFromNumber)
|
|
58
|
+
expect(result).toBe(42)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './object'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { objectMerge } from './'
|
|
2
|
+
import { objectMerge } from './object'
|
|
3
3
|
|
|
4
|
-
describe('merge
|
|
5
|
-
it('
|
|
4
|
+
describe('merge 函数', () => {
|
|
5
|
+
it('应该合并两个简单对象', () => {
|
|
6
6
|
const target = { a: 1 } as object
|
|
7
7
|
const source = { b: 2 } as object
|
|
8
8
|
const result = objectMerge(target, source)
|
|
@@ -10,7 +10,7 @@ describe('merge function', () => {
|
|
|
10
10
|
expect(result).toEqual({ a: 1, b: 2 })
|
|
11
11
|
})
|
|
12
12
|
|
|
13
|
-
it('
|
|
13
|
+
it('应该合并嵌套对象', () => {
|
|
14
14
|
const target = { a: 1, b: { c: 2 } } as object
|
|
15
15
|
const source = { b: { d: 3 }, e: 4 } as object
|
|
16
16
|
const result = objectMerge(target, source)
|
|
@@ -18,7 +18,7 @@ describe('merge function', () => {
|
|
|
18
18
|
expect(result).toEqual({ a: 1, b: { c: 2, d: 3 }, e: 4 })
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
it('
|
|
21
|
+
it('应该合并多个对象', () => {
|
|
22
22
|
const target = { a: 1 } as object
|
|
23
23
|
const source1 = { b: 2 } as object
|
|
24
24
|
const source2 = { c: 3 } as object
|
|
@@ -27,7 +27,7 @@ describe('merge function', () => {
|
|
|
27
27
|
expect(result).toEqual({ a: 1, b: 2, c: 3 })
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
it('
|
|
30
|
+
it('应该覆盖现有属性', () => {
|
|
31
31
|
const target = { a: 1, b: 2 } as object
|
|
32
32
|
const source = { b: 3, c: 4 } as object
|
|
33
33
|
const result = objectMerge(target, source)
|
|
@@ -35,7 +35,7 @@ describe('merge function', () => {
|
|
|
35
35
|
expect(result).toEqual({ a: 1, b: 3, c: 4 })
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
it('
|
|
38
|
+
it('应该处理 null 和数组值', () => {
|
|
39
39
|
const target = { a: 1, b: null } as object
|
|
40
40
|
const source = { b: [1, 2, 3], c: 4 }
|
|
41
41
|
const result = objectMerge(target, source)
|
|
@@ -43,7 +43,7 @@ describe('merge function', () => {
|
|
|
43
43
|
expect(result).toEqual({ a: 1, b: [1, 2, 3], c: 4 })
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
-
it('
|
|
46
|
+
it('应该不改变原始目标对象', () => {
|
|
47
47
|
const target = { a: 1 } as object
|
|
48
48
|
const source = { b: 2 } as object
|
|
49
49
|
const result = objectMerge(target, source)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './string'
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { randomString } from './string'
|
|
3
|
+
|
|
4
|
+
describe('randomString 函数', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// 重置随机数生成器的模拟
|
|
7
|
+
vi.clearAllMocks()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('应该生成指定长度的字符串', () => {
|
|
11
|
+
const result = randomString(10)
|
|
12
|
+
expect(result).toHaveLength(10)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('应该为零长度生成空字符串', () => {
|
|
16
|
+
const result = randomString(0)
|
|
17
|
+
expect(result).toBe('')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('应该在未指定时使用默认字符集', () => {
|
|
21
|
+
const result = randomString(100)
|
|
22
|
+
const defaultCharset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
23
|
+
|
|
24
|
+
// 检查结果只包含默认字符集中的字符
|
|
25
|
+
for (const char of result) {
|
|
26
|
+
expect(defaultCharset).toContain(char)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('应该在提供时使用自定义字符集', () => {
|
|
31
|
+
const customCharset = '123'
|
|
32
|
+
const result = randomString(10, customCharset)
|
|
33
|
+
|
|
34
|
+
// 检查结果只包含自定义字符集中的字符
|
|
35
|
+
for (const char of result) {
|
|
36
|
+
expect(customCharset).toContain(char)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('应该在多次调用时生成不同的字符串', () => {
|
|
41
|
+
const result1 = randomString(20)
|
|
42
|
+
const result2 = randomString(20)
|
|
43
|
+
|
|
44
|
+
// 虽然理论上可能相同,但实际上几乎不可能
|
|
45
|
+
expect(result1).not.toBe(result2)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('应该对单字符字符集正常工作', () => {
|
|
49
|
+
const result = randomString(5, 'A')
|
|
50
|
+
expect(result).toBe('AAAAA')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('应该对字符集中的特殊字符正常工作', () => {
|
|
54
|
+
const specialCharset = '!@#$%^&*()'
|
|
55
|
+
const result = randomString(10, specialCharset)
|
|
56
|
+
|
|
57
|
+
expect(result).toHaveLength(10)
|
|
58
|
+
|
|
59
|
+
// 检查结果只包含特殊字符集中的字符
|
|
60
|
+
for (const char of result) {
|
|
61
|
+
expect(specialCharset).toContain(char)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('应该对字符集中的Unicode字符正常工作', () => {
|
|
66
|
+
const unicodeCharset = '你好世界🎉'
|
|
67
|
+
const result = randomString(5, unicodeCharset)
|
|
68
|
+
|
|
69
|
+
expect(result).toHaveLength(5)
|
|
70
|
+
|
|
71
|
+
// 检查结果只包含Unicode字符集中的字符
|
|
72
|
+
for (const char of result) {
|
|
73
|
+
expect(unicodeCharset).toContain(char)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('应该处理大长度值', () => {
|
|
78
|
+
const result = randomString(1000)
|
|
79
|
+
expect(result).toHaveLength(1000)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('应该对固定的Math.random具有确定性', () => {
|
|
83
|
+
// 模拟Math.random返回固定值
|
|
84
|
+
const mockRandom = vi.spyOn(Math, 'random')
|
|
85
|
+
mockRandom.mockReturnValueOnce(0) // 第一个字符
|
|
86
|
+
mockRandom.mockReturnValueOnce(0.5) // 第二个字符
|
|
87
|
+
|
|
88
|
+
const charset = 'ABC'
|
|
89
|
+
const result = randomString(2, charset)
|
|
90
|
+
|
|
91
|
+
// 基于模拟的随机值,应该返回特定的字符
|
|
92
|
+
// Math.floor(0 * 3) = 0 -> 'A'
|
|
93
|
+
// Math.floor(0.5 * 3) = 1 -> 'B'
|
|
94
|
+
expect(result).toBe('AB')
|
|
95
|
+
|
|
96
|
+
mockRandom.mockRestore()
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './throttle'
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { throttle, throttleFn } from './throttle'
|
|
3
|
+
|
|
4
|
+
describe('throttle', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.restoreAllMocks()
|
|
11
|
+
vi.useRealTimers()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('throttleFn', () => {
|
|
15
|
+
it('应该立即执行第一次调用', () => {
|
|
16
|
+
const mockFn = vi.fn().mockReturnValue('result')
|
|
17
|
+
const throttledFn = throttleFn(mockFn, 100)
|
|
18
|
+
|
|
19
|
+
const result = throttledFn('arg1', 'arg2')
|
|
20
|
+
|
|
21
|
+
expect(mockFn).toHaveBeenCalledOnce()
|
|
22
|
+
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2')
|
|
23
|
+
expect(result).toBe('result')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('应该在间隔时间内忽略后续调用', () => {
|
|
27
|
+
const mockFn = vi.fn().mockReturnValue('result')
|
|
28
|
+
const throttledFn = throttleFn(mockFn, 100)
|
|
29
|
+
|
|
30
|
+
// 第一次调用应该立即执行
|
|
31
|
+
throttledFn('first')
|
|
32
|
+
expect(mockFn).toHaveBeenCalledOnce()
|
|
33
|
+
|
|
34
|
+
// 在间隔时间内的调用应该被忽略
|
|
35
|
+
throttledFn('second')
|
|
36
|
+
throttledFn('third')
|
|
37
|
+
expect(mockFn).toHaveBeenCalledOnce()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('应该在间隔时间结束后允许新的调用', () => {
|
|
41
|
+
const mockFn = vi.fn().mockReturnValue('result')
|
|
42
|
+
const throttledFn = throttleFn(mockFn, 100)
|
|
43
|
+
|
|
44
|
+
// 第一次调用
|
|
45
|
+
throttledFn('first')
|
|
46
|
+
expect(mockFn).toHaveBeenCalledOnce()
|
|
47
|
+
|
|
48
|
+
// 推进时间超过间隔
|
|
49
|
+
vi.advanceTimersByTime(150)
|
|
50
|
+
|
|
51
|
+
// 现在应该允许新的调用
|
|
52
|
+
throttledFn('second')
|
|
53
|
+
expect(mockFn).toHaveBeenCalledTimes(2)
|
|
54
|
+
expect(mockFn).toHaveBeenLastCalledWith('second')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('应该支持 leading: false 选项', () => {
|
|
58
|
+
const mockFn = vi.fn().mockReturnValue('result')
|
|
59
|
+
const throttledFn = throttleFn(mockFn, 100, { leading: false })
|
|
60
|
+
|
|
61
|
+
// 第一次调用不应该立即执行
|
|
62
|
+
throttledFn('first')
|
|
63
|
+
expect(mockFn).not.toHaveBeenCalled()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('应该支持 trailing: true 选项', () => {
|
|
67
|
+
const mockFn = vi.fn().mockReturnValue('result')
|
|
68
|
+
const throttledFn = throttleFn(mockFn, 100, { trailing: true })
|
|
69
|
+
|
|
70
|
+
// 第一次调用
|
|
71
|
+
throttledFn('first')
|
|
72
|
+
expect(mockFn).toHaveBeenCalledOnce()
|
|
73
|
+
|
|
74
|
+
// 在间隔时间内的调用
|
|
75
|
+
throttledFn('second')
|
|
76
|
+
throttledFn('third')
|
|
77
|
+
|
|
78
|
+
// 推进时间,应该执行 trailing 调用
|
|
79
|
+
vi.advanceTimersByTime(100)
|
|
80
|
+
expect(mockFn).toHaveBeenCalledTimes(2)
|
|
81
|
+
expect(mockFn).toHaveBeenLastCalledWith('third')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('应该支持 trailing: false 选项', () => {
|
|
85
|
+
const mockFn = vi.fn().mockReturnValue('result')
|
|
86
|
+
const throttledFn = throttleFn(mockFn, 100, { trailing: false })
|
|
87
|
+
|
|
88
|
+
// 第一次调用
|
|
89
|
+
throttledFn('first')
|
|
90
|
+
expect(mockFn).toHaveBeenCalledOnce()
|
|
91
|
+
|
|
92
|
+
// 在间隔时间内的调用
|
|
93
|
+
throttledFn('second')
|
|
94
|
+
throttledFn('third')
|
|
95
|
+
|
|
96
|
+
// 推进时间,不应该执行 trailing 调用
|
|
97
|
+
vi.advanceTimersByTime(100)
|
|
98
|
+
expect(mockFn).toHaveBeenCalledOnce()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('应该保持 this 上下文', () => {
|
|
102
|
+
class TestClass {
|
|
103
|
+
value = 'test-value'
|
|
104
|
+
|
|
105
|
+
getValue() {
|
|
106
|
+
return this.value
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const instance = new TestClass()
|
|
111
|
+
const throttledMethod = throttleFn(instance.getValue, 100)
|
|
112
|
+
|
|
113
|
+
const result = throttledMethod.call(instance)
|
|
114
|
+
expect(result).toBe('test-value')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('应该返回最后一次执行的结果', () => {
|
|
118
|
+
const mockFn = vi.fn()
|
|
119
|
+
.mockReturnValueOnce('first')
|
|
120
|
+
.mockReturnValueOnce('second')
|
|
121
|
+
|
|
122
|
+
const throttledFn = throttleFn(mockFn, 100)
|
|
123
|
+
|
|
124
|
+
const result1 = throttledFn()
|
|
125
|
+
expect(result1).toBe('first')
|
|
126
|
+
|
|
127
|
+
// 在间隔时间内,应该返回缓存的结果
|
|
128
|
+
const result2 = throttledFn()
|
|
129
|
+
expect(result2).toBe('first')
|
|
130
|
+
|
|
131
|
+
// 推进时间后的新调用
|
|
132
|
+
vi.advanceTimersByTime(150)
|
|
133
|
+
const result3 = throttledFn()
|
|
134
|
+
expect(result3).toBe('second')
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('throttle decorator', () => {
|
|
139
|
+
it('应该装饰类方法', () => {
|
|
140
|
+
class TestClass {
|
|
141
|
+
callCount = 0
|
|
142
|
+
|
|
143
|
+
@throttle(100)
|
|
144
|
+
increment() {
|
|
145
|
+
this.callCount++
|
|
146
|
+
return this.callCount
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const instance = new TestClass()
|
|
151
|
+
|
|
152
|
+
// 第一次调用应该立即执行
|
|
153
|
+
const result1 = instance.increment()
|
|
154
|
+
expect(result1).toBe(1)
|
|
155
|
+
expect(instance.callCount).toBe(1)
|
|
156
|
+
|
|
157
|
+
// 快速连续调用应该被节流
|
|
158
|
+
instance.increment()
|
|
159
|
+
instance.increment()
|
|
160
|
+
expect(instance.callCount).toBe(1)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('应该为每个实例独立工作', () => {
|
|
164
|
+
class TestClass {
|
|
165
|
+
callCount = 0
|
|
166
|
+
|
|
167
|
+
@throttle(100)
|
|
168
|
+
increment() {
|
|
169
|
+
this.callCount++
|
|
170
|
+
return this.callCount
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const instance1 = new TestClass()
|
|
175
|
+
const instance2 = new TestClass()
|
|
176
|
+
|
|
177
|
+
instance1.increment()
|
|
178
|
+
instance2.increment()
|
|
179
|
+
|
|
180
|
+
expect(instance1.callCount).toBe(1)
|
|
181
|
+
expect(instance2.callCount).toBe(1)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('应该支持带选项的装饰器', () => {
|
|
185
|
+
class TestClass {
|
|
186
|
+
callCount = 0
|
|
187
|
+
|
|
188
|
+
@throttle(100, { leading: false, trailing: true })
|
|
189
|
+
increment() {
|
|
190
|
+
this.callCount++
|
|
191
|
+
return this.callCount
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const instance = new TestClass()
|
|
196
|
+
|
|
197
|
+
// leading: false,第一次调用不应该立即执行
|
|
198
|
+
instance.increment()
|
|
199
|
+
expect(instance.callCount).toBe(0)
|
|
200
|
+
|
|
201
|
+
// trailing: true,应该在延迟后执行
|
|
202
|
+
vi.advanceTimersByTime(100)
|
|
203
|
+
expect(instance.callCount).toBe(1)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('应该处理带参数的方法', () => {
|
|
207
|
+
class TestClass {
|
|
208
|
+
lastArgs: any[] = []
|
|
209
|
+
|
|
210
|
+
@throttle(100)
|
|
211
|
+
saveArgs(...args: any[]) {
|
|
212
|
+
this.lastArgs = args
|
|
213
|
+
return args.length
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const instance = new TestClass()
|
|
218
|
+
|
|
219
|
+
const result = instance.saveArgs(1, 2, 3)
|
|
220
|
+
expect(result).toBe(3)
|
|
221
|
+
expect(instance.lastArgs).toEqual([1, 2, 3])
|
|
222
|
+
|
|
223
|
+
// 在间隔时间内的调用应该被忽略
|
|
224
|
+
instance.saveArgs(4, 5, 6, 7)
|
|
225
|
+
expect(instance.lastArgs).toEqual([1, 2, 3]) // 应该保持原值
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 节流装饰器 - 基于 TC39 Stage 3 Decorators 提案
|
|
3
|
+
* 限制函数在指定时间内最多执行一次
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface ThrottleOptions {
|
|
7
|
+
leading?: boolean // 是否在开始时立即执行,默认 true
|
|
8
|
+
trailing?: boolean // 是否在结束时执行最后一次,默认 true
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 通用节流函数
|
|
12
|
+
export function throttleFn<T extends (...args: any[]) => any>(
|
|
13
|
+
fn: T,
|
|
14
|
+
wait: number,
|
|
15
|
+
options: ThrottleOptions = {}
|
|
16
|
+
): T {
|
|
17
|
+
const { leading = true, trailing = true } = options
|
|
18
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
19
|
+
let lastCallTime: number = 0
|
|
20
|
+
let lastArgs: any[]
|
|
21
|
+
let lastThis: any
|
|
22
|
+
let result: any
|
|
23
|
+
|
|
24
|
+
function throttled(this: any, ...args: any[]) {
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
|
|
27
|
+
// 如果是第一次调用且不允许 leading
|
|
28
|
+
if (lastCallTime === 0 && !leading) {
|
|
29
|
+
lastCallTime = now
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const remaining = wait - (now - lastCallTime)
|
|
33
|
+
lastArgs = args
|
|
34
|
+
lastThis = this
|
|
35
|
+
|
|
36
|
+
if (remaining <= 0 || remaining > wait) {
|
|
37
|
+
if (timeout) {
|
|
38
|
+
clearTimeout(timeout)
|
|
39
|
+
timeout = null
|
|
40
|
+
}
|
|
41
|
+
lastCallTime = now
|
|
42
|
+
result = fn.apply(this, args)
|
|
43
|
+
} else if (!timeout && trailing) {
|
|
44
|
+
timeout = setTimeout(() => {
|
|
45
|
+
lastCallTime = leading ? Date.now() : 0
|
|
46
|
+
timeout = null
|
|
47
|
+
result = fn.apply(lastThis, lastArgs)
|
|
48
|
+
}, remaining)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return throttled as T
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// TC39 Stage 3 装饰器实现
|
|
58
|
+
export function throttle<T extends (...args: any[]) => any>(
|
|
59
|
+
wait: number,
|
|
60
|
+
options?: ThrottleOptions
|
|
61
|
+
) {
|
|
62
|
+
return function (originalMethod: T, context: ClassMethodDecoratorContext): T {
|
|
63
|
+
let methodKey = context.name
|
|
64
|
+
context.addInitializer(function () {
|
|
65
|
+
const self = this as any
|
|
66
|
+
// 只 patch 原型上的原始方法,避免多层嵌套
|
|
67
|
+
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(self), methodKey)
|
|
68
|
+
const method = descriptor?.value ?? originalMethod
|
|
69
|
+
const throttledMethod = throttleFn(method.bind(self), wait, options)
|
|
70
|
+
self[methodKey] = throttledMethod
|
|
71
|
+
})
|
|
72
|
+
return originalMethod
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type { ThrottleOptions }
|
package/test-exports.js
ADDED
package/tsconfig.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
|
4
|
-
|
|
5
4
|
/* Projects */
|
|
6
5
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
|
7
6
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
|
@@ -9,9 +8,8 @@
|
|
|
9
8
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
|
10
9
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
|
11
10
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
|
12
|
-
|
|
13
11
|
/* Language and Environment */
|
|
14
|
-
"target": "
|
|
12
|
+
"target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
|
15
13
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
|
16
14
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
|
17
15
|
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
|
@@ -20,14 +18,13 @@
|
|
|
20
18
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
|
21
19
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
|
22
20
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
|
23
|
-
// "noLib": true,
|
|
21
|
+
// "noLib": true, a /* Disable including any library files, including the default lib.d.ts. */
|
|
24
22
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
|
25
23
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
|
26
|
-
|
|
27
24
|
/* Modules */
|
|
28
|
-
"module": "ESNext",
|
|
25
|
+
"module": "ESNext", /* Specify what module code is generated. */
|
|
29
26
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
|
30
|
-
"moduleResolution": "node",
|
|
27
|
+
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
|
31
28
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
|
32
29
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
|
33
30
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
|
@@ -37,19 +34,17 @@
|
|
|
37
34
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
|
38
35
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
|
39
36
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
|
40
|
-
|
|
41
37
|
/* JavaScript Support */
|
|
42
38
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
|
43
39
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
|
44
40
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
|
45
|
-
|
|
46
41
|
/* Emit */
|
|
47
|
-
"declaration": true,
|
|
48
|
-
"declarationMap": true,
|
|
42
|
+
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
|
43
|
+
"declarationMap": true, /* Create sourcemaps for d.ts files. */
|
|
49
44
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
|
50
45
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
|
51
46
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
|
52
|
-
|
|
47
|
+
"outDir": "./output", /* Specify an output folder for all emitted files. */
|
|
53
48
|
// "removeComments": true, /* Disable emitting comments. */
|
|
54
49
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
|
55
50
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
|
@@ -67,22 +62,20 @@
|
|
|
67
62
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
|
68
63
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
|
69
64
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
|
70
|
-
|
|
71
65
|
/* Interop Constraints */
|
|
72
66
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
|
73
67
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
|
74
|
-
"esModuleInterop": true,
|
|
68
|
+
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
|
75
69
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
|
76
|
-
"forceConsistentCasingInFileNames": true,
|
|
77
|
-
|
|
70
|
+
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
|
78
71
|
/* Type Checking */
|
|
79
|
-
"strict": true,
|
|
72
|
+
"strict": true, /* Enable all strict type-checking options. */
|
|
80
73
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
|
81
74
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
|
82
75
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
|
83
76
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
|
84
77
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
|
85
|
-
|
|
78
|
+
"noImplicitThis": false, /* Enable error reporting when 'this' is given the type 'any'. */
|
|
86
79
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
|
87
80
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
|
88
81
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
|
@@ -95,9 +88,11 @@
|
|
|
95
88
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
|
96
89
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
|
97
90
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
|
98
|
-
|
|
99
91
|
/* Completeness */
|
|
100
92
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
|
101
|
-
"skipLibCheck": true
|
|
102
|
-
}
|
|
93
|
+
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
|
94
|
+
},
|
|
95
|
+
"include": [
|
|
96
|
+
"source"
|
|
97
|
+
]
|
|
103
98
|
}
|
package/vitest.config.ts
ADDED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export function debounce(wait: number) {
|
|
2
|
-
function debounce<T extends (this: any, ...args: any[]) => Promise<any>>(originalMethod: T, context: ClassMethodDecoratorContext): T {
|
|
3
|
-
let timeout: unknown = null
|
|
4
|
-
|
|
5
|
-
async function replacementMethod(this: any, ...args: any[]): Promise<any> {
|
|
6
|
-
return new Promise((resolve, reject) => {
|
|
7
|
-
if (timeout) clearTimeout(timeout as any)
|
|
8
|
-
timeout = setTimeout(() => {
|
|
9
|
-
originalMethod.call(this, ...args)
|
|
10
|
-
.then(resolve)
|
|
11
|
-
.catch(reject)
|
|
12
|
-
}, wait)
|
|
13
|
-
})
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
context.addInitializer(function () {
|
|
17
|
-
const self = this as any
|
|
18
|
-
self[context.name] = replacementMethod.bind(this)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
return replacementMethod as T
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return debounce
|
|
25
|
-
}
|