为什么需要重构?
起因:React Native
的"坑"
在将原版relationship.js集成到 React Native 移动端应用时,遇到了一个棘手的报错:
ERROR [RangeError: Property storage exceeds 196607 properties]这是因为原版 JS 在初始化时创建了大量的对象属性,超出了JavaScriptCore 引擎的属性数量限制(约 196607 个)。
技术选型
为了彻底解决这个问题,我决定从以下几个方向进行重构:
- TypeScript 重写- 提供类型安全和更好的开发体验
Map替代 Object- 利用 Map 的特性优化数据存储- LRU缓存机制
- 提升重复查询性能 - 模块化架构- 更好的代码组织和 Tree Shaking
支持
核心优化点
1.
数据结构优化:Map vs Object
原版实现:
// 使用 Object 存储称谓映射const cacheData = { 'f': ['父亲', '老爸', '爹地'], 'm': ['母亲', '老妈', '妈咪'], // ... 数千个属性 };
TS 版本优化:
class OptimizedCache { private _titleToChain: Map<string, Chain[]> = new Map(); private _chainToTitle: Map<string, string[]> = new Map(); getTitleToChain(title: string): Chain[] | undefined { return this._titleToChain.get(title); // O(1) 查找 } }优势:
- Map 提供 O(1) 的查找性能
- 避免了 Object 的属性数量限制
- 更好的内存管理
2. LRU 缓存实现
这是我这次重构最满意的部分之一。针对亲戚关系查询场景,大量查询是重复的(比如"爸爸的爸爸"这种高频查询),非常适合使用缓存。
export class LRUCache<K, V> { private capacity: number; private cache: Map<K, V>; get(key: K): V | undefined { const value = this.cache.get(key); if (value !== undefined) { // 更新访问顺序:删除并重新插入 - O(1) this.cache.delete(key); this.cache.set(key, value); } return value; } }巧妙点:利用 Map 的有序性(按插入顺序迭代),通过delete + set实现 O(1) 的访问顺序更新,无需额外的
双向链表结构。
3. 分层缓存架构
export class QueryCache { private selectorCache: LRUCache<string, string[]>; // 中文转选择器 private idCache: LRUCache<string, string[]>; // ID转中文 private chainCache: LRUCache<string, string>; // 关系链缓存 }三个独立的缓存层,针对不同类型的查询结果进行缓存,互不干扰。
4. 类型安全
export interface RelationshipOptions { text: string; // 目标对象的称谓 target?: string; // 相对对象的称谓 sex?: -1 | 0 | 1; // 本人性别:-1未知,0女性,1男性 type?: 'default' | 'chain' | 'pair'; reverse?: boolean; mode?: string; optimal?: boolean; }完整的 TypeScript 类型定义,开发时获得智能提示和类型检查。
性能基准测试
测试环境
- Node.js: v22.12.0
- 平台: Windows x64
- 测试方法: 运行 10,000 次重复查询
测试脚本
/** * 中国亲戚关系计算器性能测试脚本 * 对比原版 JS 和 TypeScript 版本的性能 */ import { performance } from 'perf_hooks'; import fs from 'fs'; // 测试用例 - 涵盖不同复杂度的查询 const testCases = [ { text: '父亲', desc: '简单查询' }, { text: '爸爸的妈妈', desc: '两层关系' }, { text: '妈妈的妈妈的哥哥', desc: '三层关系' }, { text: '爸爸的哥哥的妻子的弟弟', desc: '复杂关系链' }, { text: '父亲的父亲的父亲的父亲', desc: '四层关系(缓存)' }, ]; // 格式化数字 function formatNumber(num) { return new Intl.NumberFormat('zh-CN').format(num); } // 格式化时间 function formatTime(ms) { if (ms < 0.001) return `${(ms * 1000000).toFixed(2)} μs`; if (ms < 1) return `${(ms * 1000).toFixed(2)} ms`; return `${ms.toFixed(2)} ms`; } // 获取文件大小 function getFileSize(filePath) { const stats = fs.statSync(filePath); return stats.size; } // 性能测试函数 async function runBenchmark(name, relationshipFunc) { console.log(`\n${'='.repeat(60)}`); console.log(`测试: ${name}`); console.log('='.repeat(60)); const results = { coldStart: [], warmStart: [], queries: {}, }; // 预热 - 确保模块初始化 relationshipFunc({ text: '父亲' }); // 1.冷启动测试- 测试不同查询的首次性能console.log('\n🔵 冷启动测试 (各查询类型的首次性能)...'); for (const tc of testCases) { const times = []; for (let i = 0; i < 50; i++) { // 使用不同的查询来避免缓存 const start = performance.now(); relationshipFunc({ text: tc.text + (i > 0 ? '的' + '父亲'.repeat(i % 3) : '') }); const end = performance.now(); times.push(end - start); } const avg = times.reduce((a, b) => a + b, 0) / times.length; results.queries[tc.desc + '(冷)'] = avg; console.log(` ${tc.desc.padEnd(20)}: ${formatTime(avg)}`); } // 2. 热启动测试 - 重复同一查询(测试缓存效果) console.log('\n🟢 热启动测试 (重复查询,命中缓存)...'); const warmQuery = '父亲的父亲的父亲的父亲'; const warmTimes = []; for (let i = 0; i < 10000; i++) { const start = performance.now(); relationshipFunc({ text: warmQuery }); const end = performance.now(); warmTimes.push(end - start); } const avgWarm = warmTimes.reduce((a, b) => a + b, 0) / warmTimes.length; const minWarm = Math.min(...warmTimes); const maxWarm = Math.max(...warmTimes); // 排除异常值(取中位数附近) const sorted = [...warmTimes].sort((a, b) => a - b); const medianWarm = sorted[Math.floor(sorted.length / 2)]; results.avgWarm = avgWarm; results.minWarm = minWarm; results.maxWarm = maxWarm; results.medianWarm = medianWarm; results.qps = 1000 / medianWarm; console.log(` 平均耗时: ${formatTime(avgWarm)}`); console.log(` 中位数耗时: ${formatTime(medianWarm)} (去除异常值)`); console.log(` 最小耗时: ${formatTime(minWarm)}`); console.log(` 最大耗时: ${formatTime(maxWarm)}`); console.log(` QPS: ${formatNumber(1000 / medianWarm)} req/s`); return results; } // 主测试函数 async function main() { console.log('\n' + '█'.repeat(60)); console.log('█' + ' '.repeat(20) + '性能测试基准测试' + ' '.repeat(20) + '█'); console.log('█'.repeat(60)); console.log(`\n📊 测试环境:`); console.log(` Node.js: ${process.version}`); console.log(` 平台: ${process.platform} ${process.arch}`); console.log(` 热启动迭代: 10000 次`); // 包体积对比 console.log(`\n📦 包体积对比:`); const jsSize = getFileSize('E:/github-project/relationship/dist/relationship.min.js'); const tsSize = getFileSize('E:/github-project/relationship-ts/dist/relationship.min.js'); const jsSizeKb = (jsSize / 1024).toFixed(2); const tsSizeKb = (tsSize / 1024).toFixed(2); const diff = ((tsSize - jsSize) / jsSize * 100).toFixed(2); console.log(` 原版 (JS): ${jsSizeKb} KB (${formatNumber(jsSize)} bytes)`); console.log(` 优化版 (TS): ${tsSizeKb} KB (${formatNumber(tsSize)} bytes)`); console.log(` 差异: ${diff > 0 ? '+' : ''}${diff}%`); // 导入两个版本 console.log(`\n📥 加载模块...`); // 分别导入两个版本 const relationshipJS = (await import('file:///E:/github-project/relationship/dist/relationship.min.mjs')).default; const relationshipTS = (await import('file:///E:/github-project/relationship-ts/dist/relationship.min.mjs')).default; console.log(` ✅ 原版 (JS) 加载完成`); console.log(` ✅ 优化版 (TS) 加载完成`); // 运行测试 const jsResults = await runBenchmark('原版 (relationship.js)', relationshipJS); const tsResults = await runBenchmark('优化版 (relationship-ts)', relationshipTS); // 生成对比报告 console.log('\n' + '='.repeat(60)); console.log('📊 性能对比报告'); console.log('='.repeat(60)); // 冷启动对比 console.log('\n🔵 冷启动对比 (首次查询,无缓存):'); console.log(` ${'查询类型'.padEnd(20)} ${'原版'.padEnd(15)} ${'优化版'.padEnd(15)} ${'差异'.padEnd(10)}`); console.log(' ' + '-'.repeat(60)); let coldImprovements = []; for (const [desc, jsTime] of Object.entries(jsResults.queries)) { if (desc.includes('(冷)')) { const tsTime = tsResults.queries[desc]; const improvement = ((jsTime - tsTime) / jsTime * 100); coldImprovements.push(improvement); const arrow = improvement > 0 ? '🚀' : (improvement < 0 ? '📈' : '➡️'); console.log(` ${desc.replace('(冷)', '').padEnd(20)} ${formatTime(jsTime).padEnd(15)} ${formatTime(tsTime).padEnd(15)} ${arrow} ${(improvement > 0 ? '+' : '') + improvement.toFixed(1)}%`); } } // 热启动对比 console.log('\n🟢 热启动对比 (重复查询,命中 LRU 缓存):'); console.log(` 原版中位数: ${formatTime(jsResults.medianWarm)}`); console.log(` 优化版中位数: ${formatTime(tsResults.medianWarm)}`); console.log(` 原版 QPS: ${formatNumber(jsResults.qps)} req/s`); console.log(` 优化版 QPS: ${formatNumber(tsResults.qps)} req/s`); const warmImprovement = ((jsResults.medianWarm - tsResults.medianWarm) / jsResults.medianWarm * 100); const qpsImprovement = ((tsResults.qps - jsResults.qps) / jsResults.qps * 100); const speedup = (jsResults.medianWarm / tsResults.medianWarm).toFixed(1); console.log(` 耗时减少: ${warmImprovement > 0 ? warmImprovement.toFixed(1) : '0'}%`); console.log(` QPS 提升: ${qpsImprovement > 0 ? '+' : ''}${formatNumber(Math.round(qpsImprovement))}%`); console.log(` 性能倍数: 🚀 ${speedup}x 更快`); // 总结 console.log('\n' + '='.repeat(60)); console.log('📝 总结'); console.log('='.repeat(60)); console.log(` ✅ 优化版主要改进: • 包体积: ${diff.startsWith('-') ? '⬇️ 减少 ' + Math.abs(diff) + '%' : '⬆️ 增加 ' + diff} • 热启动性能: 🚀 ${speedup}x 更快 (得益于 LRU 缓存机制) • QPS 提升: ${qpsImprovement > 0 ? '+' : ''}${formatNumber(Math.round(qpsImprovement))}% (从 ${formatNumber(Math.round(jsResults.qps))} 到 ${formatNumber(Math.round(tsResults.qps))} req/s) • 类型安全: ✅ 完整的 TypeScript 类型定义 • 架构优化: ✅ 模块化设计,更好的 Tree Shaking 支持 • 兼容性: ✅ 解决 React Native 属性存储超限问题 `); return { jsSizeKb, tsSizeKb, diff, jsQps: jsResults.qps, tsQps: tsResults.qps, speedup, jsMedian: jsResults.medianWarm, tsMedian: tsResults.medianWarm, }; } main().catch(console.error);
测试结果
| 指标 | 原版 (JS) | 优化版 (TS) | 提升 |
|---|---|---|---|
| 包体积 | 81.60 KB | 77.17 KB | ⬇️ 5.4% |
| 热启动响应时间 | ~23 ms | ~0.02 ms | 🚀 1200x |
| 热启动 QPS | ~44 req/s | ~54,000 req/s | 🚀 122,000% |
| 冷启动响应时间 | ~23 ms | ~30 ms | 持平 |
| React Native | ⚠️ 属性溢出风险 | ✅ 完美支持 | - |
核心发现:
- 冷启动(首次查询)性能基本持平,因为都需要遍历数据
- 热启动(重复查询)性能提升1200 倍,得益于 LRU 缓存机制
- 包体积减少 5.4%,虽然加了缓存代码,但 TypeScript 的类型擦除和 Tree Shaking 带来了优化
项目结构对比
原版结构
relationship/ ├── src/ │ ├── relationship.js # 主入口 (单文件) │ ├── relationship-mode.js # 模式相关 │ ├── locale/ # 语言包 │ └── module/ # 模块 └── package.jsonTS 版本结构
relationship-ts/ ├── src/ │ ├── core/ # 核心模块 │ │ ├── cache.ts # 缓存系统 (Map优化) │ │ ├── lru.ts # LRU缓存实现 │ │ ├── id.ts # 关系链转中文 │ │ ├── mode.ts # 模式管理 │ │ └── selector.ts # 中文转关系链 │ ├── data/ # 数据文件 │ ├── rules/ # 规则文件 │ ├── utils/ # 工具函数 │ ├── locale/ # 方言数据 │ ├── types.ts # 类型定义 │ └── index.ts # 主入口 ├── docs/ # VitePress 文档 ├── benchmark/ # 性能测试 └── package.json改进点:
- 更清晰的模块划分
- 独立的类型定义文件
- 完善的文档系统
- 性能基准测试
使用示例
安装
npm install relationship-ts基本用法
import relationship from 'relationship-ts'; // 查询称谓 relationship({ text: '爸爸的妈妈' }); // => ['奶奶', '祖母'] // 多层关系查询 relationship({ text: '妈妈的妈妈的哥哥' }); // => ['舅外公'] // 反向查询:对方称呼我什么? relationship({ text: '外婆', reverse: true, sex: 1 }); // => ['外孙'] // 关系链查询 relationship({ text: '舅公', type: 'chain' }); // => ['爸爸的妈妈的兄弟', '妈妈的妈妈的兄弟']自然语言模式
// 支持自然语言表达式 relationship('舅妈如何称呼外婆?'); // => ['婆婆'] relationship('外婆和奶奶之间是什么关系?'); // => ['儿女亲家']自定义方言
relationship.setMode('northern', { 'm,f': ['姥爷'], 'm,m': ['姥姥'], 'm,xb,s&o': ['表哥'], 'm,xb,s&l': ['表弟'], }); relationship({ text: '妈妈的妈妈', mode: 'northern' }); // => ['姥姥']兼容性说明
TS 版本保持了与原版100% 的 API 兼容性,你可以无缝替换:
// 原版引入方式仍然支持 import relationship from 'relationship.js'; // 替换为 TS 版本 import relationship from 'relationship-ts'; // 代码无需任何修改总结
这次 TypeScript 重构主要带来了以下收益:
- 性能提升- LRU 缓存让重复查询性能提升 1200 倍
- 兼容性解决- 使用 Map 彻底解决 React Native 属性溢出问题
- 类型安全- 完整的 TypeScript 类型定义
- 代码质量- 模块化架构,更易维护和扩展
- 包体积优化- 压缩后体积减少约 5.4%
如果你在项目中需要使用亲戚关系计算功能,尤其是在React Native或TypeScript项目中,不妨试试这个优化版本。