业务需求:
首先看下实际操作:我们需要在每次改变画布中元素位置或属性后实现操作的撤销,撤销之后还要能回退。
bg (2).gif
这个看似简单的需求,思路不对的话,会走很多弯路,比如之前我用一个数组加两个游标(变量)试图去解决这个问题,但根本没可能。实现这个功能需要一些算法和栈的运用。
我们需要两个栈来实现这个操作:分别是撤销栈和回退栈。
整体流程:
1.每当我们开始拖拽画布元素,或者开始缩放之前,我们要保存此次操作时刻的屏幕快照,在本系统中,组件都以json数组的形式来渲染。所以我们需要保存的就是当前画布的组件json数组。.将当前画布的json数组作为一个整体push进撤销栈,表示记录本次操作。
2.我们拖动元素这个动作结束了,当前画布的json数组必然发生了更新。现在我们想撤销这次拖动操作。点击撤销后,先将当前画布的json数组push到回退栈中,用撤销栈中最后一个json数组来setstate重新渲染画布元素(第一步push的那个json数组)。再使用数组pop方法,删除撤销栈中最后一个json数组。
3.回退时先将当前画布的json数组push到撤销栈中,用回退栈中最后一个json数组来setstate重新渲染画布元素(第二步push的那个json数组)。再使用数组pop方法,删除回退栈中最后一个json数组。
有点绕是不是?
4f73703bd58543449a08ef6d2b2da1af.png
上图:
再看下控制台输出来理解一下:
adcaf891cd4341f9b9f6f951451cd181.gif
撤销回退实现部分:
import { cloneDeep } from 'lodash';
import { useModel } from 'umi';
// 最大回退操作步骤
const maxStep = 60;
const undoQueue: any = [];
let redoQueue: any = [];
// 实现撤销回退队列的管理功能
const QueueManager = () => {
const { components, setComponents } = useModel('visualPage');
const saveSnap = () => {
const snap = cloneDeep(components);
if (undoQueue.length >= maxStep) {
undoQueue.shift();
}
undoQueue.push(snap);
// console.log('queue', undoQueue, redoQueue);
// 注意!!每次执行命令时清空重做栈
redoQueue = [];
};
const undo = () => {
const snap = cloneDeep(components);
const c = cloneDeep(undoQueue[undoQueue.length - 1]);
setComponents(c);
redoQueue.push(snap);
undoQueue.pop();
// console.log('undo', undoQueue, redoQueue);
};
const redo = () => {
const snap = cloneDeep(components);
const c = cloneDeep(redoQueue[redoQueue.length - 1]);
setComponents(c);
undoQueue.push(snap);
redoQueue.pop();
// console.log('redo', undoQueue, redoQueue);
};
return {
saveSnap,
undo,
redo,
undoQueue,
redoQueue,
};
};
export default QueueManager;