历史记录:撤销重做功能实现原理
引言
在现代表单应用中,用户经常会遇到需要撤销(Undo)或重做(Redo)操作的需求。无论是误操作、临时改变主意,还是需要对比不同版本的表单数据,撤销重做功能都能显著提升用户体验。本文将深入探讨 TanStack Form 库中撤销重做功能的实现原理,从设计思想到具体实现细节。
撤销重做功能的核心概念
命令模式(Command Pattern)
撤销重做功能通常基于命令模式实现,该模式将操作封装为对象,使得可以参数化客户端的不同请求、队列或日志请求,以及支持可撤销的操作。
备忘录模式(Memento Pattern)
备忘录模式用于捕获和存储对象的内部状态,以便稍后可以恢复到这个状态。
TanStack Form 的撤销重做实现
状态管理架构
TanStack Form 使用基于 Store 的状态管理架构,这是实现撤销重做功能的基础:
// 表单状态结构
interface FormState<TFormData> {
values: TFormData; // 表单值
errorMap: ValidationErrorMap; // 错误映射
fieldMetaBase: Record<string, AnyFieldMetaBase>; // 字段元数据
isSubmitting: boolean; // 提交状态
isSubmitted: boolean; // 已提交状态
isValidating: boolean; // 验证状态
submissionAttempts: number; // 提交尝试次数
isSubmitSuccessful: boolean; // 提交成功状态
}
重置(Reset)机制
TanStack Form 提供了 reset() 方法作为基础的撤销功能实现:
// FormApi.ts 中的 reset 方法实现
reset = (values?: TFormData, opts?: { keepDefaultValues?: boolean }) => {
const currentFieldMeta = this.baseStore.state.fieldMetaBase;
const fieldMetaBase = this.resetFieldMeta(currentFieldMeta);
// 重置表单状态到初始值或指定值
this.baseStore.setState((prev) => ({
...getDefaultFormState({
...this.options.defaultState,
values: values ?? this.options.defaultValues ?? ({} as TFormData),
}),
fieldMetaBase,
_force_re_eval: !prev._force_re_eval,
}));
// 更新默认值(除非指定保持原默认值)
if (values && !opts?.keepDefaultValues) {
this.update({ defaultValues: values });
}
}
字段级重置
除了整个表单的重置,还支持字段级的撤销操作:
// 字段级重置实现
resetField = <TField extends DeepKeys<TFormData>>(field: TField) => {
const defaultValue = getBy(
this.options.defaultValues ?? ({} as TFormData),
field
);
// 设置字段值为默认值
this.setValue(field, defaultValue as any);
// 重置字段元数据
this.resetFieldMeta(field);
}
实现完整撤销重做栈
历史记录管理
要实现完整的撤销重做功能,需要维护一个操作历史栈:
class FormHistoryManager<TFormData> {
private history: FormStateSnapshot<TFormData>[] = [];
private redoStack: FormStateSnapshot<TFormData>[] = [];
private currentIndex = -1;
private maxHistorySize = 50;
// 创建状态快照
createSnapshot(formApi: FormApi<TFormData>): FormStateSnapshot<TFormData> {
return {
values: { ...formApi.state.values },
fieldMetaBase: { ...formApi.baseStore.state.fieldMetaBase },
timestamp: Date.now(),
};
}
// 添加操作到历史记录
pushSnapshot(snapshot: FormStateSnapshot<TFormData>) {
// 如果当前不是最新状态,清空重做栈
if (this.currentIndex < this.history.length - 1) {
this.redoStack = [];
}
this.history.push(snapshot);
this.currentIndex = this.history.length - 1;
// 限制历史记录大小
if (this.history.length > this.maxHistorySize) {
this.history.shift();
this.currentIndex--;
}
}
// 撤销操作
undo(formApi: FormApi<TFormData>): boolean {
if (this.currentIndex <= 0) return false;
const currentSnapshot = this.history[this.currentIndex];
this.redoStack.push(currentSnapshot);
this.currentIndex--;
const previousSnapshot = this.history[this.currentIndex];
this.restoreSnapshot(formApi, previousSnapshot);
return true;
}
// 重做操作
redo(formApi: FormApi<TFormData>): boolean {
if (this.redoStack.length === 0) return false;
const nextSnapshot = this.redoStack.pop()!;
this.currentIndex++;
// 如果历史记录中已有这个位置,替换它
if (this.currentIndex < this.history.length) {
this.history[this.currentIndex] = nextSnapshot;
} else {
this.history.push(nextSnapshot);
}
this.restoreSnapshot(formApi, nextSnapshot);
return true;
}
// 恢复快照
private restoreSnapshot(
formApi: FormApi<TFormData>,
snapshot: FormStateSnapshot<TFormData>
) {
// 批量更新表单状态
batch(() => {
// 恢复表单值
formApi.baseStore.setState(prev => ({
...prev,
values: snapshot.values,
}));
// 恢复字段元数据
Object.keys(snapshot.fieldMetaBase).forEach(fieldName => {
formApi.resetFieldMeta(fieldName as any);
});
});
}
}
操作监听与自动记录
为了实现自动的历史记录,需要监听表单的各种操作:
// 操作类型定义
type FormOperation =
| { type: 'SET_VALUE'; field: string; previousValue: any; newValue: any }
| { type: 'ADD_ARRAY_ITEM'; field: string; index: number; value: any }
| { type: 'REMOVE_ARRAY_ITEM'; field: string; index: number; value: any }
| { type: 'RESET'; previousState: FormStateSnapshot<any> };
// 操作监听器实现
class OperationListener {
private historyManager: FormHistoryManager<any>;
private formApi: FormApi<any>;
private debounceTimer: any;
constructor(formApi: FormApi<any>, historyManager: FormHistoryManager<any>) {
this.formApi = formApi;
this.historyManager = historyManager;
this.setupListeners();
}
private setupListeners() {
// 监听字段值变化
this.formApi.store.subscribe((state, prevState) => {
this.onFormStateChange(state, prevState);
});
// 监听特定操作
const originalSetValue = this.formApi.setValue;
this.formApi.setValue = (field: any, value: any, options?: any) => {
const previousValue = this.formApi.getValue(field);
const result = originalSetValue.call(this.formApi, field, value, options);
// 记录 SET_VALUE 操作
if (previousValue !== value) {
this.recordOperation({
type: 'SET_VALUE',
field,
previousValue,
newValue: value,
});
}
return result;
};
}
private onFormStateChange(state: any, prevState: any) {
// 防抖处理,避免频繁记录
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.recordStateSnapshot();
}, 300);
}
private recordOperation(operation: FormOperation) {
// 根据操作类型决定是否立即记录快照
if (operation.type === 'SET_VALUE') {
this.recordStateSnapshot();
}
}
private recordStateSnapshot() {
const snapshot = this.historyManager.createSnapshot(this.formApi);
this.historyManager.pushSnapshot(snapshot);
}
}
性能优化策略
增量快照
为了减少内存使用,可以采用增量快照策略:
interface DeltaSnapshot {
timestamp: number;
changes: {
field: string;
previousValue: any;
newValue: any;
}[];
baseSnapshot?: FormStateSnapshot<any>; // 基准快照引用
}
class DeltaHistoryManager {
private fullSnapshots: FormStateSnapshot<any>[] = [];
private deltaSnapshots: DeltaSnapshot[] = [];
private currentFullIndex = 0;
private snapshotInterval = 20; // 每20个操作保存一个完整快照
createDeltaSnapshot(previousState: any, currentState: any): DeltaSnapshot {
const changes: DeltaSnapshot['changes'] = [];
// 比较两个状态,找出变化的字段
Object.keys(currentState.values).forEach(field => {
if (!deepEqual(previousState.values[field], currentState.values[field])) {
changes.push({
field,
previousValue: previousState.values[field],
newValue: currentState.values[field],
});
}
});
return {
timestamp: Date.now(),
changes,
};
}
restoreFromDelta(
baseSnapshot: FormStateSnapshot<any>,
deltas: DeltaSnapshot[]
): FormStateSnapshot<any> {
const restored = { ...baseSnapshot };
deltas.forEach(delta => {
delta.changes.forEach(change => {
setBy(restored.values, change.field as any, change.newValue);
});
});
return restored;
}
}
操作合并
对于连续的相关操作,可以进行合并处理:
class OperationBatcher {
private batchOperations: FormOperation[] = [];
private batchTimeout: any;
private readonly BATCH_DELAY = 1000; // 1秒内的操作合并
addOperation(operation: FormOperation) {
this.batchOperations.push(operation);
// 重置计时器
clearTimeout(this.batchTimeout);
this.batchTimeout = setTimeout(() => {
this.processBatch();
}, this.BATCH_DELAY);
}
private processBatch() {
if (this.batchOperations.length === 0) return;
// 合并相同字段的连续SET_VALUE操作
const mergedOperations = this.mergeOperations();
// 记录合并后的操作
this.recordMergedOperations(mergedOperations);
this.batchOperations = [];
}
private mergeOperations(): FormOperation[] {
const merged: Map<string, FormOperation> = new Map();
this.batchOperations.forEach(op => {
if (op.type === 'SET_VALUE') {
// 只保留同一个字段的最后一次操作
merged.set(op.field, op);
} else {
// 其他操作不合并
merged.set(`${op.type}-${Date.now()}`, op);
}
});
return Array.from(merged.values());
}
}
用户体验考虑
撤销重做界限指示
提供视觉反馈,让用户知道是否可以撤销或重做:
// 撤销重做状态钩子
function useUndoRedo(formApi: FormApi<any>) {
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
useEffect(() => {
const unsubscribe = formApi.store.subscribe(() => {
setCanUndo(historyManager.canUndo());
setCanRedo(historyManager.canRedo());
});
return unsubscribe;
}, [formApi]);
return { canUndo, canRedo };
}
// UI组件示例
function UndoRedoButtons() {
const { canUndo, canRedo } = useUndoRedo(formApi);
return (
<div className="undo-redo-container">
<button
onClick={() => historyManager.undo(formApi)}
disabled={!canUndo}
title="撤销 (Ctrl+Z)"
>
↶ 撤销
</button>
<button
onClick={() => historyManager.redo(formApi)}
disabled={!canRedo}
title="重做 (Ctrl+Y)"
>
↷ 重做
</button>
</div>
);
}
键盘快捷键支持
集成键盘快捷键提升用户体验:
// 键盘快捷键处理
function setupKeyboardShortcuts(formApi: FormApi<any>) {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.ctrlKey || event.metaKey) {
switch (event.key) {
case 'z':
if (event.shiftKey) {
// Ctrl+Shift+Z 重做
event.preventDefault();
historyManager.redo(formApi);
} else {
// Ctrl+Z 撤销
event.preventDefault();
historyManager.undo(formApi);
}
break;
case 'y':
// Ctrl+Y 重做
event.preventDefault();
historyManager.redo(formApi);
break;
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [formApi]);
}
总结
TanStack Form 的撤销重做功能实现基于以下几个核心原则:
- 状态快照管理:通过定期保存表单状态的完整或增量快照
- 操作追踪:监听表单操作并记录操作历史
- 性能优化:采用增量快照和操作合并策略减少内存占用
- 用户体验:提供视觉反馈和键盘快捷键支持
这种实现方式既保证了功能的完整性,又考虑了性能和用户体验的平衡。开发者可以根据具体需求选择适合的实现策略,从简单的重置功能到完整的操作历史管理。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



