diff --git a/packages/core/src/path/sanitizeFileName/__tests__/index.ts b/packages/core/src/path/sanitizeFileName/__tests__/index.ts index 3347fcb..ddf56db 100644 --- a/packages/core/src/path/sanitizeFileName/__tests__/index.ts +++ b/packages/core/src/path/sanitizeFileName/__tests__/index.ts @@ -8,6 +8,6 @@ describe("slash", async () => { expect(sanitizeFileName("C:\\a a\\b b.txt")).toStrictEqual("C:\\a a\\b b.txt"); }); it("转换特殊字符", async () => { - expect(sanitizeFileName("C:\\a.*?a\\b b.txt")).toStrictEqual("C:\\a.__a\\b b.txt"); + expect(sanitizeFileName("C:\\a:.*?a\\b b.txt")).toStrictEqual("C:\\a_.__a\\b b.txt"); }); }); diff --git a/packages/core/src/throttle/__tests__/index.test.ts b/packages/core/src/throttle/__tests__/index.test.ts new file mode 100644 index 0000000..695e3d7 --- /dev/null +++ b/packages/core/src/throttle/__tests__/index.test.ts @@ -0,0 +1,120 @@ +import { throttle } from '../index' + +describe('throttle', () => { + let callback: ReturnType + + beforeEach(() => { + // 在每个测试用例前重置 mock 函数 + callback = vi.fn() + // 重置定时器模拟 + vi.useFakeTimers() + }) + + afterEach(() => { + // 清理定时器模拟 + vi.restoreAllMocks() + }) + + it('应该在指定时间内只执行一次', () => { + const throttled = throttle(callback, 1000) + + // 连续调用多次 + throttled() + throttled() + throttled() + + expect(callback).toHaveBeenCalledTimes(1) + + // 前进 500ms + vi.advanceTimersByTime(500) + throttled() + expect(callback).toHaveBeenCalledTimes(1) + + // 前进到 1000ms + vi.advanceTimersByTime(500) + throttled() + expect(callback).toHaveBeenCalledTimes(2) + }) + + it('应该正确传递参数和this上下文', () => { + const context = { value: 'test' } + const throttled = throttle(function(this: any, arg: string) { + expect(this).toBe(context) + expect(arg).toBe('arg') + callback() + }, 1000) + + throttled.call(context, 'arg') + expect(callback).toHaveBeenCalledTimes(1) + }) + + it('应该在leading=false时忽略第一次调用', () => { + const throttled = throttle(callback, 1000, { leading: false }) + + throttled() + expect(callback).not.toHaveBeenCalled() + + vi.advanceTimersByTime(1000) + throttled() + expect(callback).toHaveBeenCalledTimes(1) + }) + + it('应该在trailing=false时忽略最后一次调用', () => { + const throttled = throttle(callback, 1000, { trailing: false }) + + throttled() + throttled() + vi.advanceTimersByTime(1000) + + expect(callback).toHaveBeenCalledTimes(1) + }) + + it('应该在取消后停止执行', () => { + const throttled = throttle(callback, 1000) + + throttled() + throttled() + throttled.cancel() + + vi.advanceTimersByTime(1000) + expect(callback).toHaveBeenCalledTimes(1) + }) + + it('应该处理边界情况下的时间间隔', () => { + const throttled = throttle(callback, 0) + + throttled() + throttled() + + expect(callback).toHaveBeenCalledTimes(2) + }) + + it('应该正确处理异步函数', async () => { + const asyncFn = vi.fn().mockResolvedValue('result') + const throttled = throttle(asyncFn, 1000) + + const result1 = await throttled() + const result2 = await throttled() + + expect(asyncFn).toHaveBeenCalledTimes(1) + expect(result1).toBe('result') + expect(result2).toBe('result') + }) + + it('应该在remaining <= 0时正确执行', () => { + const throttled = throttle(callback, 1000) + + throttled() + throttled() + expect(callback).toHaveBeenCalledTimes(1) + vi.advanceTimersByTime(1000) + + throttled() + throttled() + expect(callback).toHaveBeenCalledTimes(2) + vi.advanceTimersByTime(200) + expect(callback).toHaveBeenCalledTimes(2) + vi.advanceTimersByTime(800) + expect(callback).toHaveBeenCalledTimes(3) + }) +}) \ No newline at end of file diff --git a/packages/core/src/throttle/index.ts b/packages/core/src/throttle/index.ts index d14937d..4277708 100644 --- a/packages/core/src/throttle/index.ts +++ b/packages/core/src/throttle/index.ts @@ -1,17 +1,54 @@ -export function throttle(fn: (...argu: T) => R, interval: number = 200) { - let last; - let timer: ReturnType | void; - return function (this: void, ...argu: T) { - const now = +new Date(); - if (last && now - last < interval) { - timer && clearTimeout(timer); +export function throttle( + fn: (...args: T) => R, + wait: number = 200, + options: { leading?: boolean; trailing?: boolean } = {} +) { + const { leading = true, trailing = true } = options; + let lastTime = 0; + let timer: ReturnType | null = null; + let lastResult: R; + + function invoke(this: any, args: T): R { + lastResult = fn.apply(this, args); + lastTime = Date.now(); + return lastResult; + } + + const throttled = function(this: any, ...args: T): R | undefined { + const now = Date.now(); + const remaining = wait - (now - lastTime); + + if (!lastTime && !leading) { + lastTime = now; + return; + } + + if (remaining <= 0) { + if (timer) { + clearTimeout(timer); + timer = null; + } + return invoke.call(this, args); + } + + if (trailing && !timer) { timer = setTimeout(() => { - last = now; - fn.apply(this, argu); - }, interval); - } else { - last = now; - fn.apply(this, argu); + timer = null; + invoke.call(this, args); + }, remaining); + } + + return lastResult; + }; + + // 添加 cancel 方法 + throttled.cancel = function() { + if (timer) { + clearTimeout(timer); + timer = null; } + lastTime = 0; }; + + return throttled; } diff --git a/readme.md b/readme.md index 20c1af6..527313a 100644 --- a/readme.md +++ b/readme.md @@ -1,27 +1,96 @@ -## 开发 +# xyx-utils 🚀 -**开发新模块** +[![License](https://img.shields.io/github/license/npmrun/xyx-utils)](https://github.com/npmrun/xyx-utils/blob/main/LICENSE) +[![GitHub Release](https://img.shields.io/github/v/release/npmrun/xyx-utils)](https://github.com/npmrun/xyx-utils/releases) -1. 在`packages\.vitepress\config.ts`增加`startsDirs`和`alias`。 -2. 在`internal\tsconfig\tsconfig.json`增加路径别名 -3. 在`vitest.shared.ts`修改别名 +模块化的工具库集合,提供跨平台、多场景的实用功能。 -**本地调试工具** +## 功能特性 -- yalc +- **多模块支持**:包含 Core、Node、Vue3 等独立模块 +- **现代化构建**:基于 Vite & TypeScript 构建 +- **完善测试**:Vitest 单元测试覆盖 +- **模块化架构**:使用 PNPM Workspace 管理多包 -据说是本地最好的link方案,有没有吹牛自己试试。 +## 安装 -**版本管理** +```bash +# 安装核心模块 +pnpm add @xyx-utils/core -- changeset -- changeset publish -- pnpm publish -r --access=public # --report-summary +# 安装Node工具 +pnpm add @xyx-utils/node -可以尝试尝试 +# 安装Vue3工具 +pnpm add @xyx-utils/vue3 +``` -**测试** +## 模块列表 -1. 运行指定模块的测试文件: `pnpm test core src/common` +| 模块名称 | 描述 | 文档地址 | +|------------|-----------------------|--------------------------| +| @xyx-utils/core | 基础工具函数集合 | [文档](./packages/core) | +| @xyx-utils/node | Node.js 运行时工具 | [文档](./packages/node) | +| @xyx-utils/vue3 | Vue3 组件工具库 | [文档](./packages/vue3) | +| @xyx-utils/shared| 共享类型定义 | [文档](./packages/shared) | + +## 快速开始 + +```typescript +// 使用核心模块 +import { formatDate } from '@xyx-utils/core'; + +const currentDate = new Date(); +const formatted = formatDate(currentDate, 'YYYY-MM-DD'); +const obj = { a: 1 }; +``` + +## 开发指南 + +### 开发新模块 + +1. **配置路径别名** + - 在 `packages/.vitepress/config.ts` 添加 `startsDirs` 和 `alias` + - 更新 `internal/tsconfig/tsconfig.json` 路径别名 + - 修改 `vitest.shared.ts` 测试配置 + +2. **本地调试** + ```bash + # 使用 yalc 进行本地依赖链接 + yalc add @xyx/core + ``` + +### 版本管理 + +使用 Changeset 进行版本控制: +```bash +pnpm changeset:add # 创建变更集 +pnpm changeset:version # 版本升级 +pnpm release # 完整发布流程 +``` + +## 测试 + +运行指定模块测试: +```bash +pnpm test core +``` + +查看测试覆盖率: +```bash +pnpm run coverage +``` + +## 贡献 + +欢迎通过 Issue 和 PR 参与贡献!请先阅读[贡献指南](./CONTRIBUTING.md)。 + +## License + +MIT © [npmrun](https://github.com/npmrun) + +--- + +📖 完整文档请访问 [项目文档](https://npmrun.github.io/xyx-utils/)