如何使用双向链表实现一个撤销重做的历史栈

36 阅读5分钟

1 历史栈原理

1.1 初始化

现在想要实现 undoredo 的功能首先

准备一个栈,用来保存所有历史数据,最开始存储一个数据,指向最新数据

然后,再准备一个指针,描述当前指向历史栈中的哪个位置,初始值为0

image-20220618192057385.png

1.2 进栈操作

当状态发生修改之后,需要把旧的状态存入历史栈中,只需要做两件事情

  • 入栈
  • pointer+1

image-20220618192208774.png

image-20220618192258912.png

1.3 undo操作

撤销,只需要干两件事情

  • pointer-1
  • pointer指向的位置就是当前要在页面上显示的位置

image-20220618192646002.png

1.4 redo操作

重做,只需要干两件事情

  • pointer+1
  • pointer指向的位置就是当前要在页面上显示的位置

image-20220618192723761.png

1.5 undo之后进栈

比如现在在1位置

image-20220618192839939.png

做了新的修改,需要进栈,那么要把1后面的栈全部清除

image-20220618193138693.png

1.6 栈大小控制

历史栈不能无限大,需要做控制,比如限制大小为5个,那么逻辑如下

image-20220618193618584.png

2 封装库

2.1 PointerDoubleLinkedList

 class LinkedNode {
 ​
   /** 上一个节点 */
   prev = null
   /** 当前节点存储的数据 */
   item
   /** 下一个节点 */
   next = null
 ​
   constructor(item) {
     this.item = item
   }
 ​
 }
 ​
 /** 双向链表 */
 class PointerDoubleLinkedList {
 ​
   /** 头节点 */
   #head
   /** 末尾节点 */
   #tail
   /** 当前指向的node */
   #pointerNode
   /** 链表长度 */
   length = 0
 ​
   /**
    * @param firstNode  第一个节点
    */
   constructor(firstNode) {
     firstNode !== undefined && this.append(firstNode)
   }
 ​
   /** 添加节点 */
   append(node) {
     const newNode = new LinkedNode(node)
     // 当前链表中一个元素都没有
     if (!this.length) {
       this.#head = this.#pointerNode = this.#tail = newNode
       this.length++
     }
     // #pointerNode 等于最后一个节点
     else if (this.#pointerNode === this.#tail) {
       this.#tail.next = newNode
       newNode.prev = this.#tail
       this.#tail = this.#pointerNode = newNode
       this.length++
     }
     // #pointerNode 不能最后一个节点,就要把 #pointerNode 之后的所有节点全部删除,再插入
     else {
       // 计算#pointerNode之后还有几个节点
       let nextNode = this.#pointerNode.next
       let size = 0
       while (nextNode) {
         size++
         nextNode = nextNode.next
       }
       // 获取被移除的节点,清除联系
       this.#pointerNode.next.prev = null
       // 重置 #tail 与 this.#pointerNode
       newNode.prev = this.#pointerNode
       this.#tail = this.#pointerNode.next = newNode
       this.#pointerNode = newNode
       // 重写长度
       this.length = this.length - size + 1
     }
 ​
   }
 ​
   /** 移除开头节点 */
   removeHead() {
     if (this.length === 0) return
     if (this.#pointerNode === this.#head) {
       this.#pointerNode = this.#head.next
       if (this.length === 1) this.#tail = null
     }
     this.#head = this.#head.next
     this.#head.prev = null
     this.length--
   }
 ​
   /** 是否存在上一个节点 */
   isPrev = () => this.#pointerNode !== this.#head
 ​
   /** 指向前一个节点 */
   prev = () => {
     if (!this.length) return false
     return this.#pointerNode = this.#pointerNode.prev
   }
 ​
   /** 是否存在下一个节点 */
   isNext = () => {
     if (!this.length) return false
     return this.#pointerNode !== this.#tail
   }
 ​
   /** 指向下一个节点 */
   next = () => this.#pointerNode = this.#pointerNode.next
 ​
   /** 获取当前指针 */
   getPointerNode = () => this.#pointerNode.item
 ​
   test() {
     console.log(this.#head)
     console.log(this.#tail)
   }
 ​
 }
 ​
 const list = new PointerDoubleLinkedList()
 ​
 try {
 ​
   list.append(0)
   list.append(1)
   list.append(2)
   list.append(3)
   list.append(4)
 ​
   assert.equal(list.getPointerNode(), 4, '错误1')
   assert.equal(list.isNext(), false, '错误2')
   assert.equal(list.isPrev(), true, '错误3')
 ​
   // 指向3
   list.isPrev() && list.prev()
   assert.equal(list.getPointerNode(), 3, '错误4')
   // 指向2
   list.isPrev() && list.prev()
   assert.equal(list.getPointerNode(), 2, '错误5')
   // 指向1
   list.isPrev() && list.prev()
   assert.equal(list.getPointerNode(), 1, '错误6')
   // 指向0
   list.isPrev() && list.prev()
   assert.equal(list.getPointerNode(), 0, '错误7')
   // 指向0
   list.isPrev() && list.prev()
   assert.equal(list.getPointerNode(), 0, '错误8')
 ​
   assert.equal(list.isNext(), true, '错误9')
   assert.equal(list.isPrev(), false, '错误10')
 ​
   list.append(10)
   list.next()
   assert.equal(list.isNext(), false, '错误11')
   assert.equal(list.getPointerNode(), 10, '错误12')
 ​
   assert.equal(list.length, 2, '错误13')
 ​
   // 验证 removeHead
   list.removeHead()
   assert.equal(list.length, 1, '错误14')
 ​
   // 全部移除
   list.removeHead()
   assert.equal(list.length, 0, '错误15')
 ​
   assert.equal(list.isNext(), false, '错误16')
   assert.equal(list.isPrev(), false, '错误17')
 ​
 } catch (e) {
   console.log(e.message)
 }

2.2 HistoryStack

 class HistoryStack {
 ​
   /** 历史栈最大可以存储多少数据 */
   #stackMaxSize
   #linkedList
 ​
   /**
    * @param firstNode       初始化的item
    * @param stackMaxSize    历史栈最大可以存储多少数据
    */
   constructor(firstNode, stackMaxSize = 20) {
     if (stackMaxSize < 1 && stackMaxSize > 100) throw new Error('stackMaxSize只能在1-100区间内')
     this.#stackMaxSize = stackMaxSize
     this.#linkedList = new PointerDoubleLinkedList(firstNode)
   }
 ​
   /** 进栈 */
   pushStack(item) {
     this.#linkedList.append(item)
     // console.log(this.#linkedList.length)
     if (this.#linkedList.length > this.#stackMaxSize) {
       this.#linkedList.removeHead()
     }
     // console.log(this.#linkedList.test())
   }
 ​
   /** 能否撤销 */
   canUndo() {
     return this.#linkedList.isPrev()
   }
 ​
   /** 能否重做 */
   canRedo() {
     return this.#linkedList.isNext()
   }
 ​
   /** 撤销 */
   undo() {
     try {
       return this.#linkedList.prev(), this.getCurrent()
     } catch (e) {
       console.error('undo fail', e)
     }
   }
 ​
   /** 重做 */
   redo() {
     try {
       return this.#linkedList.next(), this.getCurrent()
     } catch (e) {
       console.error('redo fail', e)
     }
   }
 ​
   /** 获取当前项 */
   getCurrent = () => this.#linkedList.getPointerNode()
 ​
   /** 获取栈大小 */
   getStackSize = () => this.#linkedList.length
 ​
 }
 ​
 try {
 ​
   const historyStack = new HistoryStack(0, 3)
 ​
   assert.equal(historyStack.canUndo(), false, '错误1')
   assert.equal(historyStack.canRedo(), false, '错误2')
 ​
   historyStack.pushStack(1)
   assert.equal(historyStack.canUndo(), true, '错误3')
   assert.equal(historyStack.canRedo(), false, '错误4')
 ​
   historyStack.pushStack(2)
   assert.equal(historyStack.getStackSize(), 3, '错误5')
 ​
   historyStack.pushStack(3)
   assert.equal(historyStack.getStackSize(), 3, '错误6')
 ​
   assert.equal(historyStack.getCurrent(), 3, '错误7')
   historyStack.undo()
   assert.equal(historyStack.getCurrent(), 2, '错误8')
   historyStack.undo()
   assert.equal(historyStack.getCurrent(), 1, '错误9')
 ​
   assert.equal(historyStack.canUndo(), false, '错误10')
 ​
 } catch (e) {
   console.log(e.message)
 }

2.3 内存泄露检测

 <!DOCTYPE html>
 <html lang="en">
 ​
 <head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Document</title>
 </head>
 ​
 <body>
   <button onclick="test()">测试是否存在内存泄露</button>
   <h1></h1>
   <script>
 ​
     class HistoryStack {
 ​
       /** 历史栈最大可以存储多少数据 */
       #stackMaxSize
       #linkedList
 ​
       /**
        * @param firstNode       初始化的item
        * @param stackMaxSize    历史栈最大可以存储多少数据
        */
       constructor(firstNode, stackMaxSize = 20) {
         if (stackMaxSize < 1 && stackMaxSize > 100) throw new Error('stackMaxSize只能在1-100区间内')
         this.#stackMaxSize = stackMaxSize
         this.#linkedList = new PointerDoubleLinkedList(firstNode)
       }
 ​
       /** 进栈 */
       pushStack(item) {
         this.#linkedList.append(item)
         // console.log(this.#linkedList.length)
         if (this.#linkedList.length > this.#stackMaxSize) {
           this.#linkedList.removeHead()
         }
         // console.log(this.#linkedList.test())
       }
 ​
       updatePointer(item) {
         this.#linkedList.updatePointer(item)
       }
 ​
       /** 能否撤销 */
       canUndo() {
         return this.#linkedList.isPrev()
       }
 ​
       /** 能否重做 */
       canRedo() {
         return this.#linkedList.isNext()
       }
 ​
       /** 撤销 */
       undo() {
         try {
           return this.#linkedList.prev(), this.getCurrent()
         } catch (e) {
           console.error('undo fail', e)
         }
       }
 ​
       /** 重做 */
       redo() {
         try {
           return this.#linkedList.next(), this.getCurrent()
         } catch (e) {
           console.error('redo fail', e)
         }
       }
 ​
       /** 获取当前项 */
       getCurrent = () => this.#linkedList.getPointerNode()
 ​
       getStackSize = () => this.#linkedList.length
 ​
     }
 ​
     class LinkedNode {
 ​
       /** 上一个节点 */
       prev = null
       /** 当前节点存储的数据 */
       item
       /** 下一个节点 */
       next = null
 ​
       constructor(item) {
         this.item = item
       }
 ​
     }
 ​
     /** 双向链表 */
     class PointerDoubleLinkedList {
 ​
       /** 头节点 */
       #head
       /** 末尾节点 */
       #tail
       /** 当前指向的node */
       #pointerNode
       /** 链表长度 */
       length = 0
 ​
       /**
        * @param firstNode  第一个节点
        */
       constructor(firstNode) {
         firstNode !== undefined && this.append(firstNode)
       }
 ​
       /** 添加节点 */
       append(node) {
         const newNode = new LinkedNode(node)
         // 当前链表中一个元素都没有
         if (!this.length) {
           this.#head = this.#pointerNode = this.#tail = newNode
           this.length++
         }
         // #pointerNode 等于最后一个节点
         else if (this.#pointerNode === this.#tail) {
           this.#tail.next = newNode
           newNode.prev = this.#tail
           this.#tail = this.#pointerNode = newNode
           this.length++
         }
         // #pointerNode 不能最后一个节点,就要把 #pointerNode 之后的所有节点全部删除,再插入
         else {
           // 计算#pointerNode之后还有几个节点
           let nextNode = this.#pointerNode.next
           let size = 0
           while (nextNode) {
             size++
             nextNode = nextNode.next
           }
           // 获取被移除的节点,清除联系
           this.#pointerNode.next.prev = null
           // 重置 #tail 与 this.#pointerNode
           newNode.prev = this.#pointerNode
           this.#tail = this.#pointerNode.next = newNode
           this.#pointerNode = newNode
           // 重写长度
           this.length = this.length - size + 1
         }
 ​
       }
 ​
       /** 更新节点 */
       updatePointer(node) {
         if (!this.length) return
         const newNode = new LinkedNode(node)
         // 只有一个节点
         if (this.length === 1) {
           this.#head = this.#tail = this.#pointerNode = newNode
         }
         // 更新pointer节点数据
         else {
           // 如果当前节点是第一个节点
           if (this.#pointerNode === this.#head) {
             this.#head.next.prev = newNode
             this.#head = this.#pointerNode = newNode
           }
           // 如果当前节点是最后一个节点
           else if (this.#pointerNode === this.#tail) {
             this.#tail.prev.next = newNode
             this.#tail = this.#pointerNode = newNode
           }
           // 中间节点
           else {
             const prevNode = this.#pointerNode.prev
             const nextNode = this.#pointerNode.next
             prevNode.next = this.#pointerNode = nextNode.prev = newNode
           }
         }
       }
 ​
       /** 移除开头节点 */
       removeHead() {
         if (this.length === 0) return
         if (this.#pointerNode === this.#head) {
           this.#pointerNode = this.#head.next
           if (this.length === 1) this.#tail = null
         }
         this.#head = this.#head.next
         this.#head.prev = null
         this.length--
       }
 ​
       /** 是否存在上一个节点 */
       isPrev = () => this.#pointerNode !== this.#head
 ​
       /** 指向前一个节点 */
       prev = () => {
         if (!this.length) return false
         return this.#pointerNode = this.#pointerNode.prev
       }
 ​
       /** 是否存在下一个节点 */
       isNext = () => {
         if (!this.length) return false
         return this.#pointerNode !== this.#tail
       }
 ​
       /** 指向下一个节点 */
       next = () => this.#pointerNode = this.#pointerNode.next
 ​
       /** 获取当前指针 */
       getPointerNode = () => this.#pointerNode.item
 ​
       test() {
         console.log(this.#head)
         console.log(this.#tail)
       }
 ​
     }
 ​
     const historyStack = new HistoryStack(0, 100)
     // historyStack.pushStack(1)
     // historyStack.undo()
     // historyStack.pushStack(2)
     // console.log(historyStack.canRedo())
     // console.log(historyStack)
 ​
     function test() {
       for (let i = 0; i < 10000; i++) {
         historyStack.pushStack([i])
         historyStack.updatePointer([Math.random()])
       }
       document.querySelector('h1').innerText = '插入成功' + Math.random()
       // console.log(historyStack)
     }
 ​
     test()
 ​
   </script>
 </body>
 ​
 </html>