历史记录:撤销重做功能实现原理

历史记录:撤销重做功能实现原理

【免费下载链接】form 🤖 Powerful and type-safe form state management for the web. TS/JS, React Form, Solid Form, Svelte Form and Vue Form. 【免费下载链接】form 项目地址: https://gitcode.com/GitHub_Trending/form/form

引言

在现代表单应用中,用户经常会遇到需要撤销(Undo)或重做(Redo)操作的需求。无论是误操作、临时改变主意,还是需要对比不同版本的表单数据,撤销重做功能都能显著提升用户体验。本文将深入探讨 TanStack Form 库中撤销重做功能的实现原理,从设计思想到具体实现细节。

撤销重做功能的核心概念

命令模式(Command Pattern)

撤销重做功能通常基于命令模式实现,该模式将操作封装为对象,使得可以参数化客户端的不同请求、队列或日志请求,以及支持可撤销的操作。

mermaid

备忘录模式(Memento Pattern)

备忘录模式用于捕获和存储对象的内部状态,以便稍后可以恢复到这个状态。

mermaid

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 的撤销重做功能实现基于以下几个核心原则:

  1. 状态快照管理:通过定期保存表单状态的完整或增量快照
  2. 操作追踪:监听表单操作并记录操作历史
  3. 性能优化:采用增量快照和操作合并策略减少内存占用
  4. 用户体验:提供视觉反馈和键盘快捷键支持

这种实现方式既保证了功能的完整性,又考虑了性能和用户体验的平衡。开发者可以根据具体需求选择适合的实现策略,从简单的重置功能到完整的操作历史管理。

【免费下载链接】form 🤖 Powerful and type-safe form state management for the web. TS/JS, React Form, Solid Form, Svelte Form and Vue Form. 【免费下载链接】form 项目地址: https://gitcode.com/GitHub_Trending/form/form

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值