Undo/Redo functionality

В Backbone.js управление состоянием приложения является одной из ключевых задач, особенно при разработке сложных интерфейсов, где пользователю необходимо поддерживать возможность отмены и повторного выполнения действий. Backbone предоставляет гибкую основу для реализации таких механизмов через модели, коллекции и события.


Основные концепции

Для реализации Undo/Redo необходимо оперировать состоянием моделей и их историей изменений. Backbone модели обладают встроенным событием change, которое позволяет отслеживать изменения атрибутов:

var Task = Backbone.Model.extend({
    defaults: {
        title: '',
        completed: false
    }
});

var task = new Task({ title: 'Пример задачи' });

task.on('change', function(model) {
    console.log('Модель изменилась', model.changedAttributes());
});

Событие change фиксирует все модификации, что позволяет сохранять состояние для последующего возврата.


Структура Undo/Redo стека

Для реализации Undo/Redo обычно используют два стека:

  • undoStack – хранит историю предыдущих состояний;
  • redoStack – хранит состояния, которые были отменены и могут быть возвращены.

Простейшая структура может выглядеть так:

var undoStack = [];
var redoStack = [];

function saveState(model) {
    undoStack.push(_.clone(model.attributes));
    redoStack = [];
}

В данном примере _.clone из библиотеки Underscore.js позволяет сохранить независимую копию текущего состояния модели. После каждой модификации необходимо вызывать saveState.


Реализация Undo

Undo восстанавливает предыдущее состояние модели:

function undo(model) {
    if (undoStack.length === 0) return;
    var prevState = undoStack.pop();
    redoStack.push(_.clone(model.attributes));
    model.set(prevState);
}

Ключевой момент — перед восстановлением состояния нужно сохранить текущее в redoStack, чтобы появилась возможность выполнить Redo.


Реализация Redo

Redo повторно применяет состояние, которое было отменено:

function redo(model) {
    if (redoStack.length === 0) return;
    var nextState = redoStack.pop();
    undoStack.push(_.clone(model.attributes));
    model.set(nextState);
}

Важно помнить, что после любого нового действия redoStack очищается, иначе будут возникать несогласованности.


Undo/Redo для коллекций

Коллекции Backbone (Backbone.Collection) требуют дополнительной логики, так как операции могут включать добавление, удаление и изменение моделей. Для отслеживания изменений коллекции удобно использовать события:

  • add — добавление модели;
  • remove — удаление модели;
  • change — изменение модели внутри коллекции.

Пример сохранения состояния коллекции:

var Tasks = Backbone.Collection.extend({
    model: Task
});

var tasks = new Tasks();

function saveCollectionState(collection) {
    undoStack.push(collection.map(model => _.clone(model.attributes)));
    redoStack = [];
}

tasks.on('add remove change', function() {
    saveCollectionState(tasks);
});

Для Undo/Redo коллекции потребуется восстанавливать набор моделей целиком:

function undoCollection(collection) {
    if (undoStack.length === 0) return;
    var prevState = undoStack.pop();
    redoStack.push(collection.map(model => _.clone(model.attributes)));
    
    collection.reset(prevState.map(attrs => new Task(attrs)));
}

Метод reset позволяет заменить все модели коллекции одной операцией, сохраняя события Backbone.


Оптимизация и расширенные возможности

  1. Пакетная фиксация изменений — объединение нескольких мелких изменений в одно действие для Undo/Redo.
  2. Глубокое клонирование — при сложных вложенных структурах атрибутов модели необходимо использовать _.cloneDeep или аналог, чтобы избежать побочных эффектов.
  3. Ограничение длины стека — для крупных приложений рекомендуется ограничивать размер undoStack и redoStack для экономии памяти.
  4. Интеграция с представлениями (Views) — при восстановлении состояния модели необходимо триггерить события и обновлять интерфейс, иначе данные модели и визуальное отображение могут расходиться.

Пример расширенного Undo/Redo для модели с атрибутами и коллекцией

var AppModel = Backbone.Model.extend({
    defaults: {
        tasks: new Backbone.Collection()
    }
});

var app = new AppModel();

var undoStack = [];
var redoStack = [];

function saveAppState() {
    undoStack.push({
        tasks: app.get('tasks').map(task => _.clone(task.attributes))
    });
    redoStack = [];
}

function undoApp() {
    if (undoStack.length === 0) return;
    var prevState = undoStack.pop();
    redoStack.push({
        tasks: app.get('tasks').map(task => _.clone(task.attributes))
    });
    app.get('tasks').reset(prevState.tasks.map(attrs => new Task(attrs)));
}

function redoApp() {
    if (redoStack.length === 0) return;
    var nextState = redoStack.pop();
    undoStack.push({
        tasks: app.get('tasks').map(task => _.clone(task.attributes))
    });
    app.get('tasks').reset(nextState.tasks.map(attrs => new Task(attrs)));
}

Такая структура позволяет одновременно управлять Undo/Redo для отдельных моделей и целых коллекций, обеспечивая консистентность состояния приложения.


Вывод

Использование стека состояний вместе с событиями Backbone позволяет реализовать полноценную систему Undo/Redo. Ключевые элементы успешной реализации:

  • отслеживание изменений через события change, add, remove;
  • хранение копий состояния моделей и коллекций;
  • корректное управление undoStack и redoStack;
  • синхронизация состояния модели с интерфейсом.

Эти принципы формируют гибкую основу для сложных интерфейсов с возможностью отмены и повторного выполнения действий.