阅读 671

(开源)给图片编辑器添加了【撤销重做】功能

本文已参与掘金创作者训练营第三期,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

前言

上篇文章我们实现了图片编辑器的辅助线,大多数的工具类型软件都支持撤销和重做,趁热打铁,我们图片编辑器的撤销重做的功能给实现一下,丰富我们的图片编辑器的功能。

演示

演示地址

rudoundo.gif

实现流程

原理讲解

通过上面的演示我们分析有这三种类型的操作,分别是push,undo,redo

1. 在操作的过程中记录状态push

image.png 通过流程图我们可以看到,正常的操作直接记录状态就可以。

2.在操作过程中撤销操作undo

image.png 通过流程图我们可以看到在撤销1操作后,当前的状态会到操作2的状态。在进一步点击撤销2会回到操作1的状态

3.在操作过程中执行重做操作rudo

image.png 通过流程图我们可以看到在操作重做1后,图片会回到操作2的状态。

4.继续在操作的过程中记录状态again push

image.png

注意:此时我们可以有两种操作,一是继续点击重做,图片会回到操作3的状态。这个我们就不画流程图了。另一种可以直接在操作图片内的元素,此时要把操作3的状态记录给移除掉.流程图如下:

代码实现

大致思路是,我们用了状态管理库,通过快照的方式,在状态变更的过程中记录到数组中,撤销的时候通过索引执向要显示的状态,重做的时候也是通过索引回复到之前状态状态。在继续操作的时候,可能会删除部分状态

定义保存记录的数据结构

// 撤销重做数据结构
undoRedoData: {
    activeSnapshot: null, // 当前激活的快照数据
    snapshots: [], // 存储的快照数据
    current: -1, // 当前索引
}
复制代码

更新快照数据

  • 我们定义了操作的类型type, 分别是push,undo,redo
// 操作类型
export type UndoRedoActionType = {
  type: 'push' | 'undo' | 'redo';
  data: DatModelItem | null;
};

复制代码
  • 执行push操作代码内容为
if (type === 'push') {
    // 深度拷贝要保存的记录
    const newData = _.cloneDeep(data);
    if (current === -1) {
      newUndoRedoData.snapshots = [...snapshots, newData];
    } else {
      // 当前已经撤销,重新操作的时候要把某些记录取消
      newUndoRedoData.snapshots = snapshots
        .slice(0, current)
        .concat([newData]);
    }
    // 重置当前激活的数据和索引
    newUndoRedoData.activeSnapshot = null;
    newUndoRedoData.current = -1;
}
复制代码

注意:由于我们用了flooks状态库,它目前不支持不可变数据,所以我们在报存记录的时候用了深拷贝,这里性能会有影响。如果不深拷贝,对象应用传递,会引发bug。之后我们会改造这个flooks,让他支持不可变数据。

  • 执行undo操作代码内容为
 if (type === 'undo') {
    // 第一次执行撤销操作
    if (current === -1) {
      newUndoRedoData.current = snapshots.length - 1;
    } else { // 连续执行撤销操作
      newUndoRedoData.current = current - 1;
    }
    // 设置当前激活的快照数据
    newUndoRedoData.activeSnapshot = snapshots[newUndoRedoData.current];
}
复制代码
  • 执行redo操作代码内容为
if (type === 'redo') {
    // 可以执行重做操作
    if (current != -1) {
      newUndoRedoData.current = current + 1;
    }
    // 重做操作已经到最后一步,重置激活的状态和索引
    if (current === snapshots.length - 1) {
      newUndoRedoData.activeSnapshot = null;
      newUndoRedoData.current = -1;
    } else {
      newUndoRedoData.activeSnapshot = snapshots[newUndoRedoData.current];
    }
}
复制代码

设置阈值,避免内存爆栈

我们一直是在往数组里添加记录,没有做上线判断,操作多的情况下可能会引发内存爆栈。设置阈值来判断记录上线。代码如下

//阈值设置100,最多报错100次操作
let threshold = 100;
// 快照数据大于阈值
if (newUndoRedoData?.snapshots?.length > threshold) {
    //保留最后的100条数据
    newUndoRedoData.snapshots = newUndoRedoData.snapshots.splice(-threshold);
}
复制代码

这里用了Arraysplice方法,改方法负值会从后向前截取。

页面逻辑

  • 撤销回退按钮
 <Tooltip placement="bottom" title="撤销">
  <Button
    onClick={undo}
    disabled={
      undoRedoData.snapshots.length === 0 || undoRedoData.current === 0
    }
    icon={<UndoOutlined />}
  />
</Tooltip>
<Tooltip placement="bottom" title="重做">
  <Button
    disabled={undoRedoData.current === -1}
    onClick={redo}
    icon={<RedoOutlined />}
  />
</Tooltip>
复制代码

这里主要注意下禁用的判断条件。

  • 主页面渲染
const getJsx = () => {
    // 有激活的快照数据说明有撤销或者重做的操作
    const data = undoRedoData.activeSnapshot || nodes;
    return data.map((item: DatModelItem) => {
      return getJsxItem(item);
    });
};
复制代码

地址

交流沟通

建立了一个微信交流群,如需沟通讨论,请加入。

image.png

二维码过期,请添加微信号q1454763497,备注image editor,我会拉你进群

总结

以上我们实现了编辑器的撤销和重做,需要注意的是我们最好要用不可变数据,用深拷贝性能不好,最好用immer,后期我们会改造。功能部分大致代码介绍上面已经描述出来,如需要查看更详细的内容,请移步fast-image-editor。 大家觉得有帮忙,请在github帮忙star一下。

历史文章

文章分类
前端
文章标签