【工具】基于链表结构实现撤销重做功能

一个文笔一般,想到哪是哪的唯心论前端小白。

🧠 - 简介

最近在研究拖拽生成表单的工具,其中有一个功能就是撤销、重做。页面大概长这么个样子:

页面还在不断的开发中,UI难看是肯定的,本文重点也不是这个。

👁️ - 分析

撤销重做功能下意识的准备选择使用数组来实现的。使用数组的 push 和 pop 再加上两个数组 undoList 和 redoList 即可实现撤销和重做的功能。

伪代码实现如下:

ts 复制代码
const undoList:NodeItem[] = [], redoList: NodeItem[] = []

// 撤销动作
const undo = () => {
    const current = undoList.pop()
    redoList.push(nodeItem)
}

// 重做动作
const redo = () => {
    const current = redoList.pop()
    undoList.push(nodeItem)
}

// 添加记录
const addHistory = (nodeItem) => {
    undoList.push(nodeItem)
    redoList.splice(0, redoList.length)
}

如上代码所示,就能实现一个撤销重做功能。

但是缺陷也很明显,如果我想限制回退最大步数,就需要再增加一个变量出来,然后我还想知道它可不可以回退与重做,就又需要增加额外的计算。

大家知道,不能小看任何一个产品经理,每个简单的需求他们都想雕朵花出来。

为了提高它的扩展性和兼容性(逼格),所以我思索再三,决定封装一个 class 类出来,并且引入单向链表的概念来实现这个功能。

链表:

我通过咨询GPT得到了链表的相关信息:

可见它非常适合现在的场景!!!

🫀 - 拆解

现在就需要声明两个类:UndoRedoUndoRedoNode

UndoRedo 就是整个功能类,根据上面的需求,在实例化这个类的时候,会传入一个 limit,用来限制最大长度,同时会暴露几个方法:addActionundoredocanUndocanRedoprintActions。然后内置几个属性:headcurrentlimitlength用来记录各个状态。

节点类就很简单了,只需要记录数据就好了。

💪 - 落实

老规矩直接上源码:

ts 复制代码
// 定义节点类
class UndoRedoNode {
  public data: string;
  public next: UndoRedoNode | null;

  constructor(data: string) {
    this.data = data;
    this.next = null;
  }
}

class UndoRedo {
  private head: UndoRedoNode | null;
  private current: UndoRedoNode | null;
  private limit: number; // 长度限制
  private length: number; // 当前长度

  constructor(limit = 10) { // 默认长度限制为10
    this.head = null;
    this.current = null;
    this.limit = limit;
    this.length = 0;
    
    // this.addAction([])
  }

  public addAction(action: any): void {
    const newNode = new UndoRedoNode(action);

    if (this.head === null) {
      this.head = newNode;
      this.current = newNode;
    } else {
      this.current!.next = newNode;
      this.current = newNode;
    }

    if (this.length < this.limit) {
      this.length++;
    } else {
      // 如果达到长度限制,移除最早的操作
      this.head = this.head!.next;
    }

    // 重置current之后的操作
    this.current!.next = null;
  }

  public undo(): any | null {
    if (!this.canUndo()) {
      return false;
    }

    let prev = this.head;
    while (prev!.next !== this.current) {
      prev = prev!.next;
    }

    this.current = prev;
    this.length--; // 更新长度
    return this.current!.data;
  }

  public redo(): any | null {
    if (!this.canRedo()) {
      return false;
    }

    this.current = this.current!.next;
    this.length++; // 更新长度
    return this.current!.data;
  }

  // 检查是否可以撤销
  public canUndo(): boolean {
    return this.current !== this.head;
  }

  // 检查是否可以重做
  public canRedo(): boolean {
    return this.current !== null && this.current.next !== null;
  }

  public printActions(): void {
    let current = this.head;
    while (current !== null) {
      console.log(current.data);
      current = current.next;
    }
  }
}

export default UndoRedo;

// 示例用法
// const undoRedo = new UndoRedo(3); // 设置长度限制为3

// undoRedo.addAction("操作A");
// undoRedo.addAction("操作B");
// undoRedo.addAction("操作C");
// undoRedo.addAction("操作D"); // 此时"操作A"会被移除

// console.log("当前操作:");
// undoRedo.printActions(); // 输出 操作B 操作C 操作D

// console.log("是否可以撤销:", undoRedo.canUndo()); // 输出 true
// console.log("是否可以重做:", undoRedo.canRedo()); // 输出 false

有一个坑需要注意一下,这个类只提供了功能,但是常规操作下,撤销到第一步的时候,应该是一个白屏,所以需要白屏阶段也存一个记录。可以看到我在 constructor 中注释了一条 this.addAction([]),这是因为我现在的页面,每一步的节点值都是一个数组,所以我就存了一个 [],当然也可以在业务逻辑中去做,例如 vuemounted 阶段。

另外要说的就是,redoundo 都会返回节点数据,直接拿来更新视图就好了,canUndocanRedo 则通过 computed 放在按钮的 disabled 属性上,用来控制按钮的可用性。

🛀 - 总结

其实在刷 力扣 的时候经常会遇到链表,什么单向链表、双向链表、环形链表,当时刷题的时候就当题目给刷过去了,但是真正的使用到业务开发中的场景却很少,这也算是一个不错的实践了!

同时,这个撤销重做完全可以被任何项目去引用,现在的是 ts 版本,如果想要在 js 中使用,稍微调整一下就好了,或者使用 ts 编译一下也可以。

相关推荐
编程零零七1 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
(⊙o⊙)~哦3 小时前
JavaScript substring() 方法
前端
无心使然云中漫步4 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者4 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_4 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
麒麟而非淇淋5 小时前
AJAX 入门 day1
前端·javascript·ajax
2401_858120535 小时前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢5 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写7 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js
史努比.7 小时前
redis群集三种模式:主从复制、哨兵、集群
前端·bootstrap·html