一个文笔一般,想到哪是哪的唯心论前端小白。
🧠 - 简介
最近在研究拖拽生成表单的工具,其中有一个功能就是撤销、重做。页面大概长这么个样子:
页面还在不断的开发中,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 编译一下也可以。