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

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

🧠 - 简介

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

页面还在不断的开发中,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得到了链表的相关信息:

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

🫀 - 拆解

现在就需要声明两个类:UndoRedo、UndoRedoNode。

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

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

💪 - 落实

老规矩直接上源码:

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([]),这是因为我现在的页面,每一步的节点值都是一个数组,所以我就存了一个 [],当然也可以在业务逻辑中去做,例如 vue 的 mounted 阶段。

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

🛀 - 总结

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

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

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师3 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学3 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端