前端快照实现方案

简介

snapshot 翻译为快照,用于直观获取页面在某个运行时的状态,将执行操作前后的快照进行存储,可以轻松实现页面状态的重做、撤销功能。

本文主要介绍 snapshot 工具实现的原理,以及其在项目中的使用。

设计

要实现页面状态的历史记录、重做、撤销,需要支持以下几个属性和方法

属性

  • 历史记录:存储历史的页面状态,包含页面初始化的状态 到 上一个页面状态
  • 撤销记录:存储重做的每一个操作记录,用于撤销后恢复
  • 当前记录:临时存储当前页面状态,主要用于下一次操作后,需要将其存储到历史记录
  • 上次插入数据时间:插入时间间隔太小时,需要额外处理
ini 复制代码
 // 历史记录
 recordList: string[] = []
 ​
 // 撤销记录,用于重做
 redoList: string[] = []
 ​
 // 当前记录用 currentRecord 变量暂时存储,当用户修改时,再存放到 recordList
 currentRecord = ''
 ​
 // 上次插入数据时间
 time = 0

方法

存储历史记录 push

当用户操作后,更新历史记录。需要考虑以下几点。

  • 当前操作时间距离上次插入时间小于 100 ms 时,则替换当前记录并取消执行添加
  • 如果当前记录有值,则说明上一次操作是手动插入,将之前缓存的当前记录推入 recordList,并且需要清空重做记录
  • 将当前状态存储到当前记录
  • 设置最大历史记录容量,当超过时,将最先插入的数据删除
kotlin 复制代码
 push(record: PageData) {
   const nowTime = Date.now()
   // 防止添加重复的时间,当添加间隔小于 100ms 时,则替换当前记录并取消执行添加
   if (this.time + 100 > nowTime) {
     try {
       // 将 json 转成字符串存储
       this.currentRecord = JSON.stringify(record)
     } catch (error) {
       return false
     }
 ​
     return false
   }
 ​
   this.time = nowTime
 ​
   // 判断之前是否已经存在currentRecord记录,有则存储到recordList
   if (this.currentRecord) {
     this.recordList.push(this.currentRecord)
     // 增加记录后则应该清空重做记录
     this.redoList.splice(0, this.redoList.length)
   }
 ​
   // 存储当前记录
   this.currentRecord = JSON.stringify(record)
 ​
   // 最多存储 30 条记录,超过则删除之前的记录
   if (this.recordList.length > 30) {
     this.recordList.unshift()
   }
   return true
 }

撤销操作 undo

当用户操作后,依赖 push 时存储的历史记录列表,将页面状态回退到上一次的状态,需要注意以下几点:

  • 当历史记录没有时,直接返回
  • 从历史记录中取出最后一次存储数据
  • 若当前记录存在,需要将其存放到重做记录列表
  • 需要清空当前记录,防止重复添加,因为撤销后,也会执行 push 存储历史记录方法
kotlin 复制代码
 undo() {
   // 没有记录时,返回 false
   if (this.recordList.length === 0) {
     return null
   }
 ​
   const record = this.recordList.pop()
 ​
   // 将当前记录添加到重做记录里面
   if (this.currentRecord) {
     this.redoList.push(this.currentRecord)
   }
 ​
   // 丢弃当前记录,防止重复添加
   this.currentRecord = ''
   return JSON.parse(record as string) as PageData
 }

重做操作 redo

当用户操作后,依赖 redoList 列表,将页面状态回退到撤销前的状态,需要注意以下几点:

  • 当重做记录没有时,直接返回
  • 从重做记录里取出最后一次存储数据
  • 如果当前记录有值,需要将其放到历史记录列表
  • 需要清空当前记录,防止重复添加,因为重做后,也会执行 push 存储历史记录方法
kotlin 复制代码
 redo() {
   // 没有重做记录时,返回 false
   if (this.redoList.length === 0) {
     return null
   }
 ​
   const record = this.redoList.pop()
   // 添加到重做记录里面
   if (this.currentRecord) {
     this.recordList.push(this.currentRecord)
   }
 ​
   // 丢弃当前记录,防止重复添加
   this.currentRecord = ''
   return JSON.parse(record as string) as PageData
 }

过程演示

假设数据列表为 [1, 2, 3, 4],当前属性值分别为:

ini 复制代码
 recordList = [1, 2, 3]
 redoList = []
 currentRecord = 4

1、手动添加 5,则会执行 push 方法,执行后属性值分别为

ini 复制代码
 recordList = [1, 2, 3, 4]
 redoList = []
 currentRecord = 5

2、执行1次撤销,则先会执行 undo,执行后属性值分别为

ini 复制代码
 recordList = [1, 2, 3]
 redoList = [5]
 currentRecord = ''

然后执行 push,将 4 push 进去,执行后属性值分别为

ini 复制代码
 recordList = [1, 2, 3]
 redoList = [5]
 currentRecord = 4

3、执行第2次撤销,则先会执行 undo,执行后属性值分别为

ini 复制代码
 recordList = [1, 2]
 redoList = [5, 4]
 currentRecord = ''

然后执行 push,将 3 push 进去,执行后属性值分别为

ini 复制代码
 recordList = [1, 2]
 redoList = [5, 4]
 currentRecord = 3

4、执行1次重做,则先会执行 redo,执行后属性值分别为

ini 复制代码
 recordList = [1, 2, 3]
 redoList = [5]
 currentRecord = ''

然后执行 push,将 4 push 进去,执行后属性值分别为

ini 复制代码
 recordList = [1, 2, 3]
 redoList = [5]
 currentRecord = 4

5、手动添加 6,则会执行 push 方法,执行后属性值分别为

ini 复制代码
 recordList = [1, 2, 3, 4]
 redoList = []
 currentRecord = 6

完整代码

kotlin 复制代码
 export default class Snapshot {
     // 历史记录
     recordList: string[] = []
 ​
     // 撤销记录,用于重做
     redoList: string[] = []
 ​
     // 当前记录用 currentRecord 变量暂时存储,当用户修改时,再存放到 recordList
     currentRecord = ''
 ​
     // 上次插入数据时间
     time = 0
 ​
     push(record: PageData) {
         const nowTime = Date.now()
         // 防止添加重复的时间,当添加间隔小于 100ms 时,则替换当前记录并取消执行添加
         if (this.time + 100 > nowTime) {
             try {
                 // 将 json 转成字符串存储
                 this.currentRecord = JSON.stringify(record)
             } catch (error) {
                 return false
             }
 ​
             return false
         }
 ​
         this.time = nowTime
 ​
         // 判断之前是否已经存在currentRecord记录,有则存储到recordList
         if (this.currentRecord) {
             this.recordList.push(this.currentRecord)
             // 增加记录后则应该清空重做记录
             this.redoList.splice(0, this.redoList.length)
         }
 ​
         try {
             // 将 json 转成字符串存储
             this.currentRecord = JSON.stringify(record)
         } catch (error) {
             return
         }
 ​
         // 最多存储 30 条记录,超过则删除之前的记录
         if (this.recordList.length > 30) {
             this.recordList.unshift()
         }
         return true
     }
 ​
     undo() {
         // 没有记录时,返回 false
         if (this.recordList.length === 0) {
             return null
         }
 ​
         const record = this.recordList.pop()
 ​
         // 将当前记录添加到重做记录里面
         if (this.currentRecord) {
             this.redoList.push(this.currentRecord)
         }
 ​
         // 丢弃当前记录,防止重复添加
         this.currentRecord = ''
         return JSON.parse(record as string) as PageData
     }
 ​
     redo() {
         // 没有重做记录时,返回 false
         if (this.redoList.length === 0) {
             return null
         }
 ​
         const record = this.redoList.pop()
         // 添加到重做记录里面
         if (this.currentRecord) {
             this.recordList.push(this.currentRecord)
         }
 ​
         // 丢弃当前记录,防止重复添加
         this.currentRecord = ''
         return JSON.parse(record as string) as PageData
     }
 }

在线演示

codepen.io/belle-peng/...

相关推荐
华洛1 小时前
讲讲如何在传统产品中挖掘AI需求
javascript·产品经理·产品
why技术1 小时前
AI Coding开始进入第四个时代,我还没上车呢!
前端·人工智能·后端
大家的林语冰2 小时前
CSS 已死?DOM 性能黑洞!Pretext 排版革命让你在文本间跳舞,没有 DOM 也能纵享丝滑~
前端·javascript·css
vipbic2 小时前
我也该升级了,陪伴了我7年的博客
前端
Lee川3 小时前
RAG 实战:从一篇掘金文章出发,拆解检索增强生成的全链路
前端·人工智能·后端
Lee川3 小时前
MCP 高德地图实战:当 AI 学会使用工具,一个协议如何重塑大模型的行动边界
前端·人工智能·后端
ZC跨境爬虫3 小时前
跟着 MDN 学CSS day_14:(尺寸调整技能测试与实战解析)
前端·css·ui·html·tensorflow
kyriewen3 小时前
用魔法打败魔法:我让AI替我去面试前端岗,AI面试官给我打了92分,还发了offer
前端·javascript·面试
IT_陈寒3 小时前
Redis批量删除踩了坑,原来DEL命令不是万能的
前端·人工智能·后端
lichenyang4534 小时前
鸿蒙聊天 Demo 练习 06:AI 思考气泡与 MVVM + Controller 结构重构
前端