🎨 Optimize code & Upgrade dependencies

This commit is contained in:
2026-01-01 02:27:21 +08:00
parent 9ec22add55
commit 76f6c30b9d
17 changed files with 316 additions and 1247 deletions

View File

@@ -1,265 +0,0 @@
/**
* 操作信息接口
*/
interface OperationInfo {
controller: AbortController;
createdAt: number;
timeout?: number;
timeoutId?: NodeJS.Timeout;
}
/**
* 异步操作管理器
* 用于管理异步操作的竞态条件,确保只有最新的操作有效
* 支持操作超时和自动清理机制
*
* @template T 操作上下文的类型
*/
export class AsyncManager<T = any> {
private operationSequence = 0;
private pendingOperations = new Map<number, OperationInfo>();
private currentContext: T | null = null;
private defaultTimeout: number;
/**
* 创建异步操作管理器
*
* @param defaultTimeout 默认超时时间毫秒0表示不设置超时
*/
constructor(defaultTimeout: number = 0) {
this.defaultTimeout = defaultTimeout;
}
/**
* 生成新的操作ID
*
* @returns 新的操作ID
*/
getNextOperationId(): number {
return ++this.operationSequence;
}
/**
* 开始新的操作
*
* @param context 操作上下文
* @param options 操作选项
* @returns 操作ID和AbortController
*/
startOperation(
context: T,
options?: {
excludeId?: number;
timeout?: number;
}
): { operationId: number; abortController: AbortController } {
const operationId = this.getNextOperationId();
const abortController = new AbortController();
const timeout = options?.timeout ?? this.defaultTimeout;
// 取消之前的操作
this.cancelPreviousOperations(options?.excludeId);
// 创建操作信息
const operationInfo: OperationInfo = {
controller: abortController,
createdAt: Date.now(),
timeout: timeout > 0 ? timeout : undefined
};
// 设置超时处理
if (timeout > 0) {
operationInfo.timeoutId = setTimeout(() => {
this.cancelOperation(operationId, 'timeout');
}, timeout);
}
// 设置当前上下文和操作
this.currentContext = context;
this.pendingOperations.set(operationId, operationInfo);
return { operationId, abortController };
}
/**
* 检查操作是否仍然有效
*
* @param operationId 操作ID
* @param context 操作上下文
* @returns 操作是否有效
*/
isOperationValid(operationId: number, context?: T): boolean {
const operationInfo = this.pendingOperations.get(operationId);
const contextValid = context === undefined || this.currentContext === context;
return (
operationInfo !== undefined &&
!operationInfo.controller.signal.aborted &&
contextValid
);
}
/**
* 完成操作
*
* @param operationId 操作ID
*/
completeOperation(operationId: number): void {
const operationInfo = this.pendingOperations.get(operationId);
if (operationInfo) {
// 清理超时定时器
if (operationInfo.timeoutId) {
clearTimeout(operationInfo.timeoutId);
}
this.pendingOperations.delete(operationId);
}
}
/**
* 取消指定操作
*
* @param operationId 操作ID
* @param reason 取消原因
*/
cancelOperation(operationId: number, reason?: string): void {
const operationInfo = this.pendingOperations.get(operationId);
if (operationInfo) {
// 清理超时定时器
if (operationInfo.timeoutId) {
clearTimeout(operationInfo.timeoutId);
}
// 取消操作
operationInfo.controller.abort(reason);
this.pendingOperations.delete(operationId);
}
}
/**
* 取消之前的操作修复并发bug
*
* @param excludeId 要排除的操作ID不取消该操作
*/
cancelPreviousOperations(excludeId?: number): void {
// 创建要取消的操作ID数组避免在遍历时修改Map
const operationIdsToCancel: number[] = [];
for (const [operationId] of this.pendingOperations) {
if (excludeId === undefined || operationId !== excludeId) {
operationIdsToCancel.push(operationId);
}
}
// 批量取消操作
for (const operationId of operationIdsToCancel) {
this.cancelOperation(operationId, 'superseded');
}
}
/**
* 取消所有操作
*/
cancelAllOperations(): void {
// 创建要取消的操作ID数组避免在遍历时修改Map
const operationIdsToCancel = Array.from(this.pendingOperations.keys());
// 批量取消操作
for (const operationId of operationIdsToCancel) {
this.cancelOperation(operationId, 'cancelled');
}
this.currentContext = null;
}
/**
* 清理过期操作(手动清理超时操作)
*
* @param maxAge 最大存活时间(毫秒)
* @returns 清理的操作数量
*/
cleanupExpiredOperations(maxAge: number): number {
const now = Date.now();
const expiredOperationIds: number[] = [];
for (const [operationId, operationInfo] of this.pendingOperations) {
if (now - operationInfo.createdAt > maxAge) {
expiredOperationIds.push(operationId);
}
}
// 批量取消过期操作
for (const operationId of expiredOperationIds) {
this.cancelOperation(operationId, 'expired');
}
return expiredOperationIds.length;
}
/**
* 获取操作统计信息
*
* @returns 操作统计信息
*/
getOperationStats(): {
total: number;
withTimeout: number;
averageAge: number;
oldestAge: number;
} {
const now = Date.now();
let withTimeout = 0;
let totalAge = 0;
let oldestAge = 0;
for (const operationInfo of this.pendingOperations.values()) {
const age = now - operationInfo.createdAt;
totalAge += age;
oldestAge = Math.max(oldestAge, age);
if (operationInfo.timeout) {
withTimeout++;
}
}
return {
total: this.pendingOperations.size,
withTimeout,
averageAge: this.pendingOperations.size > 0 ? totalAge / this.pendingOperations.size : 0,
oldestAge
};
}
/**
* 获取当前上下文
*
* @returns 当前上下文
*/
getCurrentContext(): T | null {
return this.currentContext;
}
/**
* 设置当前上下文
*
* @param context 新的上下文
*/
setCurrentContext(context: T | null): void {
this.currentContext = context;
}
/**
* 获取待处理操作数量
*
* @returns 待处理操作数量
*/
get pendingCount(): number {
return this.pendingOperations.size;
}
/**
* 检查是否有待处理的操作
*
* @returns 是否有待处理的操作
*/
hasPendingOperations(): boolean {
return this.pendingOperations.size > 0;
}
}

View File

@@ -1,42 +1,13 @@
import { LanguageType } from '@/../bindings/voidraft/internal/models/models';
import type { SupportedLocaleType } from '@/common/constant/locales';
/**
* 配置工具类
*/
export class ConfigUtils {
/**
* 将后端语言类型转换为前端语言代码
*/
static backendLanguageToFrontend(language: LanguageType): SupportedLocaleType {
return language === LanguageType.LangZhCN ? 'zh-CN' : 'en-US';
}
/**
* 将前端语言代码转换为后端语言类型
*/
static frontendLanguageToBackend(locale: SupportedLocaleType): LanguageType {
return locale === 'zh-CN' ? LanguageType.LangZhCN : LanguageType.LangEnUS;
}
/**
* 验证数值是否在指定范围内
*/
static clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
/**
* 验证数值是否在指定范围内
*/
static clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
/**
* 验证配置值是否有效
*/
static isValidConfigValue<T>(value: T, validValues: readonly T[]): boolean {
return validValues.includes(value);
}
/**
* 获取配置的默认值
*/
static getDefaultValue<T>(key: string, defaults: Record<string, { default: T }>): T {
return defaults[key]?.default;
}
}

View File

@@ -1,329 +0,0 @@
/**
* DOM Diff 算法单元测试
*/
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
import { morphNode, morphHTML, morphWithKeys } from './domDiff';
describe('DOM Diff Algorithm', () => {
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
describe('morphNode - 基础功能', () => {
test('应该更新文本节点内容', () => {
const fromNode = document.createTextNode('Hello');
const toNode = document.createTextNode('World');
container.appendChild(fromNode);
morphNode(fromNode, toNode);
expect(fromNode.nodeValue).toBe('World');
});
test('应该保持相同的文本节点不变', () => {
const fromNode = document.createTextNode('Hello');
const toNode = document.createTextNode('Hello');
container.appendChild(fromNode);
const originalNode = fromNode;
morphNode(fromNode, toNode);
expect(fromNode).toBe(originalNode);
expect(fromNode.nodeValue).toBe('Hello');
});
test('应该替换不同类型的节点', () => {
const fromNode = document.createElement('span');
fromNode.textContent = 'Hello';
const toNode = document.createElement('div');
toNode.textContent = 'World';
container.appendChild(fromNode);
morphNode(fromNode, toNode);
expect(container.firstChild?.nodeName).toBe('DIV');
expect(container.firstChild?.textContent).toBe('World');
});
});
describe('morphNode - 属性更新', () => {
test('应该添加新属性', () => {
const fromEl = document.createElement('div');
const toEl = document.createElement('div');
toEl.setAttribute('class', 'test');
toEl.setAttribute('id', 'myid');
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.getAttribute('class')).toBe('test');
expect(fromEl.getAttribute('id')).toBe('myid');
});
test('应该更新已存在的属性', () => {
const fromEl = document.createElement('div');
fromEl.setAttribute('class', 'old');
const toEl = document.createElement('div');
toEl.setAttribute('class', 'new');
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.getAttribute('class')).toBe('new');
});
test('应该删除不存在的属性', () => {
const fromEl = document.createElement('div');
fromEl.setAttribute('class', 'test');
fromEl.setAttribute('id', 'myid');
const toEl = document.createElement('div');
toEl.setAttribute('class', 'test');
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.getAttribute('class')).toBe('test');
expect(fromEl.hasAttribute('id')).toBe(false);
});
});
describe('morphNode - 子节点更新', () => {
test('应该添加新子节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = '<li>1</li><li>2</li>';
const toEl = document.createElement('ul');
toEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.children.length).toBe(3);
expect(fromEl.children[2].textContent).toBe('3');
});
test('应该删除多余的子节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
const toEl = document.createElement('ul');
toEl.innerHTML = '<li>1</li><li>2</li>';
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.children.length).toBe(2);
expect(fromEl.textContent).toBe('12');
});
test('应该更新子节点内容', () => {
const fromEl = document.createElement('div');
fromEl.innerHTML = '<p>Old</p>';
const toEl = document.createElement('div');
toEl.innerHTML = '<p>New</p>';
container.appendChild(fromEl);
const originalP = fromEl.querySelector('p');
morphNode(fromEl, toEl);
// 应该保持同一个 p 元素,只更新内容
expect(fromEl.querySelector('p')).toBe(originalP);
expect(fromEl.querySelector('p')?.textContent).toBe('New');
});
});
describe('morphHTML - HTML 字符串更新', () => {
test('应该从 HTML 字符串更新元素', () => {
const element = document.createElement('div');
element.innerHTML = '<p>Old</p>';
container.appendChild(element);
morphHTML(element, '<p>New</p>');
expect(element.innerHTML).toBe('<p>New</p>');
});
test('应该处理复杂的 HTML 结构', () => {
const element = document.createElement('div');
element.innerHTML = '<h1>Title</h1><p>Paragraph</p>';
container.appendChild(element);
morphHTML(element, '<h1>New Title</h1><p>New Paragraph</p><span>Extra</span>');
expect(element.children.length).toBe(3);
expect(element.querySelector('h1')?.textContent).toBe('New Title');
expect(element.querySelector('p')?.textContent).toBe('New Paragraph');
expect(element.querySelector('span')?.textContent).toBe('Extra');
});
});
describe('morphWithKeys - 基于 key 的智能 diff', () => {
test('应该保持相同 key 的节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="a">A Updated</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
container.appendChild(fromEl);
const originalA = fromEl.querySelector('[data-key="a"]');
morphWithKeys(fromEl, toEl);
expect(fromEl.querySelector('[data-key="a"]')).toBe(originalA);
expect(originalA?.textContent).toBe('A Updated');
});
test('应该重新排序节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="c">C</li>
<li data-key="a">A</li>
<li data-key="b">B</li>
`;
container.appendChild(fromEl);
morphWithKeys(fromEl, toEl);
const keys = Array.from(fromEl.children).map(child => child.getAttribute('data-key'));
expect(keys).toEqual(['c', 'a', 'b']);
});
test('应该添加新的 key 节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
container.appendChild(fromEl);
morphWithKeys(fromEl, toEl);
expect(fromEl.children.length).toBe(3);
expect(fromEl.querySelector('[data-key="c"]')?.textContent).toBe('C');
});
test('应该删除不存在的 key 节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="c">C</li>
`;
container.appendChild(fromEl);
morphWithKeys(fromEl, toEl);
expect(fromEl.children.length).toBe(2);
expect(fromEl.querySelector('[data-key="b"]')).toBeNull();
});
});
describe('性能测试', () => {
test('应该高效处理大量节点', () => {
const fromEl = document.createElement('ul');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fromEl.appendChild(li);
}
const toEl = document.createElement('ul');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Updated Item ${i}`;
toEl.appendChild(li);
}
container.appendChild(fromEl);
const startTime = performance.now();
morphNode(fromEl, toEl);
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(100); // 应该在 100ms 内完成
expect(fromEl.children.length).toBe(1000);
expect(fromEl.children[0].textContent).toBe('Updated Item 0');
});
});
describe('边界情况', () => {
test('应该处理空节点', () => {
const fromEl = document.createElement('div');
const toEl = document.createElement('div');
container.appendChild(fromEl);
expect(() => morphNode(fromEl, toEl)).not.toThrow();
});
test('应该处理只有文本的节点', () => {
const fromEl = document.createElement('div');
fromEl.textContent = 'Hello';
const toEl = document.createElement('div');
toEl.textContent = 'World';
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.textContent).toBe('World');
});
test('应该处理嵌套的复杂结构', () => {
const fromEl = document.createElement('div');
fromEl.innerHTML = `
<div class="outer">
<div class="inner">
<span>Text</span>
</div>
</div>
`;
const toEl = document.createElement('div');
toEl.innerHTML = `
<div class="outer modified">
<div class="inner">
<span>Updated Text</span>
<strong>New</strong>
</div>
</div>
`;
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.querySelector('.outer')?.classList.contains('modified')).toBe(true);
expect(fromEl.querySelector('span')?.textContent).toBe('Updated Text');
expect(fromEl.querySelector('strong')?.textContent).toBe('New');
});
});
});

View File

@@ -1,180 +0,0 @@
/**
* 轻量级 DOM Diff 算法实现
* 基于 morphdom 思路,只更新变化的节点,保持未变化的节点不动
*/
/**
* 比较并更新两个 DOM 节点
* @param fromNode 原节点
* @param toNode 目标节点
*/
export function morphNode(fromNode: Node, toNode: Node): void {
// 节点类型不同,直接替换
if (fromNode.nodeType !== toNode.nodeType || fromNode.nodeName !== toNode.nodeName) {
fromNode.parentNode?.replaceChild(toNode.cloneNode(true), fromNode);
return;
}
// 文本节点:比较内容
if (fromNode.nodeType === Node.TEXT_NODE) {
if (fromNode.nodeValue !== toNode.nodeValue) {
fromNode.nodeValue = toNode.nodeValue;
}
return;
}
// 元素节点:更新属性和子节点
if (fromNode.nodeType === Node.ELEMENT_NODE) {
const fromEl = fromNode as Element;
const toEl = toNode as Element;
// 更新属性
morphAttributes(fromEl, toEl);
// 更新子节点
morphChildren(fromEl, toEl);
}
}
/**
* 更新元素属性
*/
function morphAttributes(fromEl: Element, toEl: Element): void {
// 移除旧属性
const fromAttrs = fromEl.attributes;
for (let i = fromAttrs.length - 1; i >= 0; i--) {
const attr = fromAttrs[i];
if (!toEl.hasAttribute(attr.name)) {
fromEl.removeAttribute(attr.name);
}
}
// 添加/更新新属性
const toAttrs = toEl.attributes;
for (let i = 0; i < toAttrs.length; i++) {
const attr = toAttrs[i];
const fromValue = fromEl.getAttribute(attr.name);
if (fromValue !== attr.value) {
fromEl.setAttribute(attr.name, attr.value);
}
}
}
/**
* 更新子节点(核心 diff 算法)
*/
function morphChildren(fromEl: Element, toEl: Element): void {
const fromChildren = Array.from(fromEl.childNodes);
const toChildren = Array.from(toEl.childNodes);
const fromLen = fromChildren.length;
const toLen = toChildren.length;
const minLen = Math.min(fromLen, toLen);
// 1. 更新公共部分
for (let i = 0; i < minLen; i++) {
morphNode(fromChildren[i], toChildren[i]);
}
// 2. 移除多余的旧节点
if (fromLen > toLen) {
for (let i = fromLen - 1; i >= toLen; i--) {
fromEl.removeChild(fromChildren[i]);
}
}
// 3. 添加新节点
if (toLen > fromLen) {
for (let i = fromLen; i < toLen; i++) {
fromEl.appendChild(toChildren[i].cloneNode(true));
}
}
}
/**
* 优化版:使用 key 进行更智能的 diff可选
* 适用于有 data-key 属性的元素
*/
export function morphWithKeys(fromEl: Element, toEl: Element): void {
const toChildren = Array.from(toEl.children) as Element[];
// 构建 from 的 key 映射
const fromKeyMap = new Map<string, Element>();
Array.from(fromEl.children).forEach((child) => {
const key = child.getAttribute('data-key');
if (key) {
fromKeyMap.set(key, child);
}
});
const processedKeys = new Set<string>();
// 按照 toChildren 的顺序处理
toChildren.forEach((toChild, toIndex) => {
const key = toChild.getAttribute('data-key');
if (!key) return;
processedKeys.add(key);
const fromChild = fromKeyMap.get(key);
if (fromChild) {
// 找到对应节点,更新内容
morphNode(fromChild, toChild);
// 确保节点在正确的位置
const currentNode = fromEl.children[toIndex];
if (currentNode !== fromChild) {
// 将 fromChild 移动到正确位置
fromEl.insertBefore(fromChild, currentNode);
}
} else {
// 新节点,插入到正确位置
const currentNode = fromEl.children[toIndex];
fromEl.insertBefore(toChild.cloneNode(true), currentNode || null);
}
});
// 删除不再存在的节点(从后往前删除,避免索引问题)
const childrenToRemove: Element[] = [];
fromKeyMap.forEach((child, key) => {
if (!processedKeys.has(key)) {
childrenToRemove.push(child);
}
});
childrenToRemove.forEach(child => {
fromEl.removeChild(child);
});
}
/**
* 高级 API直接从 HTML 字符串更新元素
*/
export function morphHTML(element: Element, htmlString: string): void {
const tempContainer = document.createElement('div');
tempContainer.innerHTML = htmlString;
// 更新元素的子节点列表
morphChildren(element, tempContainer);
}
/**
* 批量更新(使用 DocumentFragment
*/
export function batchMorph(element: Element, htmlString: string): void {
const tempContainer = document.createElement('div');
tempContainer.innerHTML = htmlString;
const fragment = document.createDocumentFragment();
Array.from(tempContainer.childNodes).forEach(node => {
fragment.appendChild(node);
});
// 清空原内容
while (element.firstChild) {
element.removeChild(element.firstChild);
}
// 批量插入
element.appendChild(fragment);
}