【PPTist】历史记录功能

前言:本篇文章来分析一下PPTist中实现历史记录功能的代码,包括快照的数据结构、主要方法 回退、重做 以及实现的思路

一、快照的定义

定义在 src/store/snapshot.ts 中,使用的是 pinia 状态管理库,和Vuex挺像的,但是要更简洁一下

  • State
    存储原始数据
    需要响应式的数据
    全局共享的数据
  • Getters
    需要根据 state 计算的值
    多处使用的计算逻辑
    需要缓存的计算结果
  • Actions
    业务逻辑封装
    异步操作
    多个状态修改的组合

分别看一下它的几个属性

1、state
① snapshotCursor

历史快照指针,表示当前快照的位置,初始值为 -1,使用示例:

bash 复制代码
初始状态:
snapshotCursor = -1
snapshots = []

添加快照A:
snapshotCursor = 0
snapshots = [A]

添加快照B:
snapshotCursor = 1
snapshots = [A, B]

添加快照C:
snapshotCursor = 2
snapshots = [A, B, C]

执行撤销:
snapshotCursor = 1
snapshots = [A, B, C]  // 回到B的状态

执行重做:
snapshotCursor = 2
snapshots = [A, B, C]  // 回到C的状态

在B的位置添加新快照D:
snapshotCursor = 2
snapshots = [A, B, D]  // C被丢弃
② snapshotLength

历史快照的长度

重做的时候,需要判断当前指针的位置和snapshotLength的关系,如果当前指针在最后一个操作了,即等于 snapshotLength-1 ,就不能重做

2、getters

类似于计算属性,包含两个属性

① canUndo

能否执行撤销操作

ts 复制代码
canUndo(state) {
  // 当指针大于0时,表示可以撤销
  return state.snapshotCursor > 0
},
② canRedo

能否执行重做操作

typescript 复制代码
canRedo(state) {
  // 当指针小于最后一个位置时,表示可以重做
  return state.snapshotCursor < state.snapshotLength - 1
},
3、actions
① initSnapshotDatabase

初始化快照

typescript 复制代码
async initSnapshotDatabase() {
  // 初始化快照数据库
  const slidesStore = useSlidesStore()

  // 创建第一个快照
  const newFirstSnapshot = {
    index: slidesStore.slideIndex,
    slides: slidesStore.slides,
  }

  // 将第一个快照添加到数据库中
  await db.snapshots.add(newFirstSnapshot)

  // 设置快照指针为0,表示当前快照在历史快照中的位置
  this.setSnapshotCursor(0)

  // 设置历史快照长度为1,表示当前快照在历史快照中的位置
  this.setSnapshotLength(1)
},
② addSnapshot

添加快照

这里有一个类似于浏览器实现回退和前进的逻辑

想象一下,假设当前的浏览过的页面的快照列表是 [A、B、C],也就是说我们依次访问了A、B、C这三个页面,并且当前位置在C页面。此时我们回退两次,到A页面,然后再跳往D页面,那么在历史记录中,B、C的痕迹就会被抹杀,点击回退会到A页面,再点击前进会到D页面,B、C是无法通过回退和前进到达的彼岸。

快照的实现逻辑也是类似的,就是说我们在添加快照的时候,要关注当前的指针的位置,指针后面的快照都要舍弃,在当前的指针的位置的后面添加一个新快照。

另外快照保存的数量限制在20条

typescript 复制代码
async addSnapshot() {
  const slidesStore = useSlidesStore()

  // 获取当前indexeddb中全部快照的ID
  const allKeys = await db.snapshots.orderBy('id').keys()

  let needDeleteKeys: IndexableTypeArray = []

  // 记录需要删除的快照ID
  // 若当前快照指针不处在最后一位,那么再添加快照时,应该将当前指针位置后面的快照全部删除,对应的实际情况是:
  // 用户撤回多次后,再进行操作(添加快照),此时原先被撤销的快照都应该被删除
  if (this.snapshotCursor >= 0 && this.snapshotCursor < allKeys.length - 1) {
    needDeleteKeys = allKeys.slice(this.snapshotCursor + 1)
  }

  // 添加新快照
  const snapshot = {
	index: slidesStore.slideIndex,
	slides: slidesStore.slides,
  }
  await db.snapshots.add(snapshot)

  // 计算当前快照长度,用于设置快照指针的位置(此时指针应该处在最后一位,即:快照长度 - 1)
  let snapshotLength = allKeys.length - needDeleteKeys.length + 1

  // 快照数量超过长度限制时,应该将头部多余的快照删除
  const snapshotLengthLimit = 20
  if (snapshotLength > snapshotLengthLimit) {
    needDeleteKeys.push(allKeys[0])
    snapshotLength--
  }

  // 批量删除
  await db.snapshots.bulkDelete(needDeleteKeys)

  // 更新指针
  this.setSnapshotCursor(snapshotLength - 1)
  this.setSnapshotLength(snapshotLength)
},
③ unDo

撤销操作,需要先判断指针的位置,如果指针小于等于0,说明操作已经被撤销完了,木有退路,退不可退,直接返回

然后如果可以撤销的话,指针需要向前挪动一位,然后获取此时的快照数据,赋值给幻灯片

typescript 复制代码
async unDo() {
  // 如果快照指针小于0,表示没有快照可以撤销
  if (this.snapshotCursor <= 0) return

  const slidesStore = useSlidesStore()
  const mainStore = useMainStore()

  const snapshotCursor = this.snapshotCursor - 1
  const snapshots: Snapshot[] = await db.snapshots.orderBy('id').toArray()
  const snapshot = snapshots[snapshotCursor]
  const { index, slides } = snapshot

  const slideIndex = index > slides.length - 1 ? slides.length - 1 : index

  slidesStore.setSlides(slides)
  slidesStore.updateSlideIndex(slideIndex)
  this.setSnapshotCursor(snapshotCursor)
  mainStore.setActiveElementIdList([])
},
④ redo

和撤销差不多的思路

typescript 复制代码
async reDo() {
  if (this.snapshotCursor >= this.snapshotLength - 1) return

  const slidesStore = useSlidesStore()
  const mainStore = useMainStore()

  const snapshotCursor = this.snapshotCursor + 1
  const snapshots: Snapshot[] = await db.snapshots.orderBy('id').toArray()
  const snapshot = snapshots[snapshotCursor]
  const { index, slides } = snapshot

  const slideIndex = index > slides.length - 1 ? slides.length - 1 : index

  slidesStore.setSlides(slides)
  slidesStore.updateSlideIndex(slideIndex)
  this.setSnapshotCursor(snapshotCursor)
  mainStore.setActiveElementIdList([])
},

其中 db.snapshotsid 是数据库自己实现的自增id

二、通过防抖节流包装历史记录操作

src/hooks/useHistorySnapshot.ts 文件使用防抖和节流包装历史记录的操作,节约性能,另外提供统一的接口实现历史记录操作。可以看到,其中的防抖和节流使用的是lodash 库提供的方法,这也是我们在实际编程中可以借鉴的一个技巧

typescript 复制代码
import { debounce, throttle} from 'lodash'
import { useSnapshotStore } from '@/store'

export default () => {
  const snapshotStore = useSnapshotStore()

  // 防抖 (debounce):
  // 当持续触发事件时,一定时间段内没有再触发事件,事件才会执行一次
  // 如果设定的时间到来之前,又一次触发了事件,就重新开始延时
  const addHistorySnapshot = debounce(function() {
    snapshotStore.addSnapshot()
  }, 300, { trailing: true })  // 300ms 内的连续调用,只执行最后一次

  // 节流 (throttle):
  // 当持续触发事件时,保证一定时间段内只调用一次事件处理函数
  const redo = throttle(function() {
    snapshotStore.reDo()
  }, 100, { leading: true, trailing: false })  // 100ms 内最多执行一次

  const undo = throttle(function() {
    snapshotStore.unDo()
  }, 100, { leading: true, trailing: false })  // 100ms 内最多执行一次

  return {
    addHistorySnapshot,
    redo,
    undo,
  }
}

三、给操作添加快照

所有的要记录到历史记录的操作,在执行操作的时候都必须手动调用 addHistorySnapshot 方法

typescript 复制代码
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
const { addHistorySnapshot } = useHistorySnapshot()
// 在执行操作的地方添加
addHistorySnapshot()

hooks 文件夹中的操作都有执行这个方法,确保所有操作都是可以撤销或重做的。所以在添加新功能的时候,也不要忘记使用这个方法记录操作快照哦🐸

相关推荐
m0_748237152 小时前
海康威视监控web实时预览解决方案
前端
清风孤客3 小时前
Vue.js 如何塑造现代网络体验
前端·网络·vue.js
licy__4 小时前
Vue 2 中 v-text 和 v-html 指令的使用详解
开发语言·前端·javascript
tester Jeffky4 小时前
深入探索 jQuery:解锁前端开发的高效工具
前端·javascript·jquery
qq_366740604 小时前
ubuntu+ros新手笔记(一)
linux·笔记·ubuntu
明明真系叻4 小时前
第二十五周机器学习笔记:卷积神经网络复习、动手深度学习—线性回归、感知机
笔记·机器学习·线性回归·1024程序员节
可乐续命!4 小时前
opencv下载安装及VS配置(笔记)
笔记·opencv
bohu835 小时前
分布式事务seata(AT)与nacos整合-笔记1
笔记·nacos·seata·安装·at
web130933203985 小时前
基于python的Selenium webdriver环境搭建(笔记)
笔记·python·selenium