前端备忘录模式fe-interview:状态保存恢复

前端备忘录模式fe-interview:状态保存恢复

【免费下载链接】fe-interview haizlin/fe-interview: 前端面试指南,包含大量的前端面试题及参考答案,适合用于准备前端面试。 【免费下载链接】fe-interview 项目地址: https://gitcode.com/GitHub_Trending/fe/fe-interview

痛点:为什么你的应用需要状态回溯能力?

在日常开发中,你是否遇到过这样的场景:

  • 用户误操作后无法恢复到之前的状态
  • 复杂的表单填写过程中需要临时保存进度
  • 需要实现撤销/重做功能但不知从何入手
  • 应用崩溃后用户数据全部丢失

这些问题的核心在于状态管理的缺失。备忘录模式(Memento Pattern)正是解决这类问题的利器,它允许在不破坏封装性的前提下捕获并外部化对象的内部状态,以便后续可以恢复到此状态。

备忘录模式核心概念

三大角色解析

mermaid

模式对比表

设计模式主要目的适用场景前端应用
备忘录模式状态保存与恢复撤销/重做、历史记录表单状态管理、编辑器
观察者模式状态变化通知事件处理、数据绑定Vue/React状态管理
策略模式算法替换条件分支处理表单验证策略

前端实战:实现可撤销的表单组件

基础实现方案

// 备忘录类 - 负责存储状态
class FormMemento {
    constructor(state) {
        this.state = JSON.parse(JSON.stringify(state)); // 深拷贝
        this.timestamp = new Date();
    }
    
    getState() {
        return this.state;
    }
}

// 原发器 - 表单状态管理者
class FormOriginator {
    constructor() {
        this.state = {
            username: '',
            email: '',
            phone: '',
            address: ''
        };
    }
    
    // 创建备忘录
    createMemento() {
        return new FormMemento(this.state);
    }
    
    // 从备忘录恢复状态
    restore(memento) {
        this.state = memento.getState();
    }
    
    // 更新状态
    updateField(field, value) {
        this.state[field] = value;
    }
    
    getState() {
        return this.state;
    }
}

// 负责人 - 管理备忘录历史
class FormCaretaker {
    constructor() {
        this.history = [];
        this.currentIndex = -1;
    }
    
    // 保存状态
    saveState(originator) {
        // 清除当前索引之后的历史
        if (this.currentIndex < this.history.length - 1) {
            this.history = this.history.slice(0, this.currentIndex + 1);
        }
        
        const memento = originator.createMemento();
        this.history.push(memento);
        this.currentIndex = this.history.length - 1;
    }
    
    // 撤销
    undo(originator) {
        if (this.currentIndex > 0) {
            this.currentIndex--;
            originator.restore(this.history[this.currentIndex]);
            return true;
        }
        return false;
    }
    
    // 重做
    redo(originator) {
        if (this.currentIndex < this.history.length - 1) {
            this.currentIndex++;
            originator.restore(this.history[this.currentIndex]);
            return true;
        }
        return false;
    }
    
    // 获取历史记录
    getHistory() {
        return this.history.map((memento, index) => ({
            index,
            timestamp: memento.timestamp,
            isCurrent: index === this.currentIndex
        }));
    }
}

React集成示例

import React, { useReducer, useCallback } from 'react';

// 使用useReducer管理复杂状态
const formReducer = (state, action) => {
    switch (action.type) {
        case 'UPDATE_FIELD':
            return {
                ...state,
                [action.field]: action.value
            };
        case 'RESTORE_STATE':
            return action.state;
        default:
            return state;
    }
};

const useFormWithHistory = (initialState) => {
    const [state, dispatch] = useReducer(formReducer, initialState);
    const [history, setHistory] = React.useState([]);
    const [currentIndex, setCurrentIndex] = React.useState(-1);

    // 保存当前状态到历史记录
    const saveState = useCallback(() => {
        const newMemento = JSON.parse(JSON.stringify(state));
        
        setHistory(prev => {
            // 清除重做历史
            const newHistory = prev.slice(0, currentIndex + 1);
            newHistory.push(newMemento);
            setCurrentIndex(newHistory.length - 1);
            return newHistory;
        });
    }, [state, currentIndex]);

    // 更新字段并自动保存
    const updateField = useCallback((field, value) => {
        dispatch({ type: 'UPDATE_FIELD', field, value });
        saveState();
    }, [saveState]);

    // 撤销
    const undo = useCallback(() => {
        if (currentIndex > 0) {
            const newIndex = currentIndex - 1;
            setCurrentIndex(newIndex);
            dispatch({ type: 'RESTORE_STATE', state: history[newIndex] });
        }
    }, [currentIndex, history]);

    // 重做
    const redo = useCallback(() => {
        if (currentIndex < history.length - 1) {
            const newIndex = currentIndex + 1;
            setCurrentIndex(newIndex);
            dispatch({ type: 'RESTORE_STATE', state: history[newIndex] });
        }
    }, [currentIndex, history]);

    return {
        state,
        updateField,
        undo,
        redo,
        canUndo: currentIndex > 0,
        canRedo: currentIndex < history.length - 1,
        history
    };
};

// 使用示例
const UserForm = () => {
    const { state, updateField, undo, redo, canUndo, canRedo } = useFormWithHistory({
        username: '',
        email: '',
        phone: ''
    });

    return (
        <div>
            <input
                value={state.username}
                onChange={(e) => updateField('username', e.target.value)}
                placeholder="用户名"
            />
            <input
                value={state.email}
                onChange={(e) => updateField('email', e.target.value)}
                placeholder="邮箱"
            />
            <input
                value={state.phone}
                onChange={(e) => updateField('phone', e.target.value)}
                placeholder="手机号"
            />
            
            <div>
                <button onClick={undo} disabled={!canUndo}>
                    撤销
                </button>
                <button onClick={redo} disabled={!canRedo}>
                    重做
                </button>
            </div>
        </div>
    );
};

性能优化策略

状态快照优化

class OptimizedMementoManager {
    constructor(maxHistorySize = 50, throttleDelay = 500) {
        this.maxHistorySize = maxHistorySize;
        this.throttleDelay = throttleDelay;
        this.history = [];
        this.currentIndex = -1;
        this.lastSaveTime = 0;
    }

    // 节流保存
    throttledSave(originator, force = false) {
        const now = Date.now();
        if (force || now - this.lastSaveTime > this.throttleDelay) {
            this.save(originator);
            this.lastSaveTime = now;
        }
    }

    // 差异保存(只保存变化的部分)
    differentialSave(originator, prevState) {
        const currentState = originator.getState();
        const changes = this.getStateChanges(prevState, currentState);
        
        if (Object.keys(changes).length > 0) {
            this.save({
                ...prevState,
                ...changes,
                __timestamp: Date.now()
            });
        }
    }

    getStateChanges(oldState, newState) {
        const changes = {};
        for (const key in newState) {
            if (oldState[key] !== newState[key]) {
                changes[key] = newState[key];
            }
        }
        return changes;
    }

    // 清理过期历史
    cleanupHistory() {
        if (this.history.length > this.maxHistorySize) {
            this.history = this.history.slice(-this.maxHistorySize);
            this.currentIndex = Math.min(this.currentIndex, this.maxHistorySize - 1);
        }
    }
}

内存管理最佳实践

策略优点缺点适用场景
完整快照实现简单,恢复准确内存占用大小型状态对象
差异保存内存效率高实现复杂,需要比较算法大型状态对象
懒保存减少不必要的保存可能丢失中间状态高频状态变化

实际应用场景分析

场景一:富文本编辑器

mermaid

场景二:图形绘制应用

class DrawingAppMemento {
    constructor(canvasState, drawingHistory, selectionState) {
        this.canvasState = canvasState;
        this.drawingHistory = [...drawingHistory]; // 绘制操作历史
        this.selectionState = selectionState;
        this.timestamp = Date.now();
    }
}

// 针对图形应用的优化备忘录
class DrawingMementoManager {
    constructor() {
        this.keyframes = new Map(); // 关键帧存储
        this.operations = []; // 操作记录
    }
    
    // 保存关键帧(完整状态)
    saveKeyframe(state) {
        const keyframe = new DrawingAppMemento(
            JSON.parse(JSON.stringify(state.canvas)),
            [...state.history],
            JSON.parse(JSON.stringify(state.selection))
        );
        this.keyframes.set(Date.now(), keyframe);
    }
    
    // 保存操作(增量变化)
    saveOperation(operation) {
        this.operations.push({
            ...operation,
            timestamp: Date.now()
        });
    }
    
    // 恢复到某个时间点
    restoreToTimestamp(timestamp) {
        // 找到最近的关键帧
        const keyframeTime = this.findNearestKeyframe(timestamp);
        const keyframe = this.keyframes.get(keyframeTime);
        
        // 重放关键帧之后的操作直到目标时间
        const operationsToReplay = this.operations.filter(op => 
            op.timestamp > keyframeTime && op.timestamp <= timestamp
        );
        
        return { keyframe, operationsToReplay };
    }
}

高级模式:分布式状态管理

与Redux集成

// 基于Redux的备忘录中间件
const mementoMiddleware = store => next => action => {
    const result = next(action);
    
    if (action.meta?.saveHistory) {
        const currentState = store.getState();
        const memento = {
            state: currentState,
            action: action.type,
            timestamp: Date.now()
        };
        
        // 保存到历史
        store.dispatch({
            type: 'HISTORY_ADD',
            payload: memento
        });
    }
    
    return result;
};

// Reducer处理历史状态
const historyReducer = (state = { history: [], currentIndex: -1 }, action) => {
    switch (action.type) {
        case 'HISTORY_ADD':
            const newHistory = state.history.slice(0, state.currentIndex + 1);
            newHistory.push(action.payload);
            return {
                history: newHistory,
                currentIndex: newHistory.length - 1
            };
        case 'HISTORY_UNDO':
            if (state.currentIndex > 0) {
                return {
                    ...state,
                    currentIndex: state.currentIndex - 1
                };
            }
            return state;
        case 'HISTORY_REDO':
            if (state.currentIndex < state.history.length - 1) {
                return {
                    ...state,
                    currentIndex: state.currentIndex + 1
                };
            }
            return state;
        default:
            return state;
    }
};

测试策略与质量保证

单元测试示例

describe('Memento Pattern', () => {
    let originator;
    let caretaker;

    beforeEach(() => {
        originator = new FormOriginator();
        caretaker = new FormCaretaker();
    });

    test('should save and restore state correctly', () => {
        // 初始状态
        originator.updateField('username', 'testuser');
        caretaker.saveState(originator);
        
        // 修改状态
        originator.updateField('username', 'modifieduser');
        
        // 恢复状态
        caretaker.undo(originator);
        expect(originator.getState().username).toBe('testuser');
    });

    test('should handle multiple undo/redo operations', () => {
        const testValues = ['state1', 'state2', 'state3'];
        
        testValues.forEach((value, index) => {
            originator.updateField('testField', value);
            caretaker.saveState(originator);
        });

        // 撤销两次
        caretaker.undo(originator);
        caretaker.undo(originator);
        expect(originator.getState().testField).toBe('state1');

        // 重做一次
        caretaker.redo(originator);
        expect(originator.getState().testField).toBe('state2');
    });

    test('should clear redo history when new action is performed', () => {
        // 创建一些历史记录
        ['a', 'b', 'c'].forEach(val => {
            originator.updateField('field', val);
            caretaker.saveState(originator);
        });

        // 撤销到第一个状态
        caretaker.undo(originator);
        caretaker.undo(originator);

        // 执行新操作应该清除重做历史
        originator.updateField('field', 'new');
        caretaker.saveState(originator);

        expect(caretaker.getHistory().length).toBe(2); // 只有初始状态和新状态
        expect(caretaker.redo(originator)).toBe(false); // 不能重做
    });
});

总结与最佳实践

备忘录模式在前端开发中提供了强大的状态管理能力,特别是在需要实现撤销/重做、状态历史、临时保存等功能的场景中。通过合理的实现和优化,可以在不显著影响性能的前提下为用户提供流畅的状态管理体验。

关键收获

  1. 封装性保持:备忘录模式允许在不暴露对象内部细节的情况下保存和恢复状态
  2. 历史管理:通过负责人(Caretaker)可以轻松管理多个状态快照
  3. 用户体验提升:为用户提供撤销、重做、历史查看等功能
  4. 数据安全:防止意外操作导致的数据丢失

实施建议

  • 对于小型状态对象,使用完整快照方式
  • 对于大型状态,考虑差异保存或懒加载策略
  • 合理设置历史记录大小限制,避免内存溢出
  • 在React/Vue等框架中,结合hooks/composition API实现更优雅

备忘录模式不仅是面试中的重要考点,更是实际开发中提升应用质量和用户体验的实用工具。掌握这一模式,让你在前端状态管理的道路上更加游刃有余。

【免费下载链接】fe-interview haizlin/fe-interview: 前端面试指南,包含大量的前端面试题及参考答案,适合用于准备前端面试。 【免费下载链接】fe-interview 项目地址: https://gitcode.com/GitHub_Trending/fe/fe-interview

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

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

抵扣说明:

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

余额充值