Lzh on GitHub

当你调用 commit() 时,手动追踪记录一个 ref 的更改历史,并提供撤销(undo)和重做(redo)功能。

核心概念

在使用 useManualRefHistory 时,你需要理解以下几个核心概念:

  • 快照 (Snapshot): 每当 ref 的值发生变化并被 “提交” 到历史记录时,都会创建一个当前值的快照。
  • 时间戳 (Timestamp): 每个快照都会记录一个时间戳,表示该快照被创建的时间。
  • 历史记录栈 (History Stacks): 为了实现撤销和重做,useManualRefHistory 内部维护了两个栈:
    • 撤销栈 (undoStack): 存储可以撤销的快照历史,最新的快照位于栈顶。
    • 重做栈 (redoStack): 存储可以重做的快照历史,通常在执行撤销操作后填充。
  • 手动提交 (Manual Commit): 与 useRefHistory(通常是自动追踪)不同,useManualRefHistory 要求你明确调用 commit() 方法来记录当前 ref 的状态到历史中。

Demo

<script setup lang="ts">
import { formatDate, useCounter, useManualRefHistory } from '@vueuse/core'

function format(ts: number) {
  return formatDate(new Date(ts), 'YYYY-MM-DD HH:mm:ss')
}

const { inc, dec, count } = useCounter()
const { canUndo, canRedo, history, commit, undo, redo } = useManualRefHistory(count, { capacity: 10 })
</script>

<template>
  <div>Count: {{ count }}</div>
  <button @click="inc()">
    Increment
  </button>
  <button @click="dec()">
    Decrement
  </button>
  <span class="ml-2">/</span>
  <button @click="commit()">
    Commit
  </button>
  <button :disabled="!canUndo" @click="undo()">
    Undo
  </button>
  <button :disabled="!canRedo" @click="redo()">
    Redo
  </button>
  <br>
  <br>
  <note>History (limited to 10 records for demo)</note>
  <div class="code-block mt-4">
    <div v-for="i in history" :key="i.timestamp">
      <span class="opacity-50 mr-2 font-mono">{{ format(i.timestamp) }}</span>
      <span class="font-mono">{ value: {{ i.snapshot }} }</span>
    </div>
  </div>
</template>

使用

import { useManualRefHistory } from '@vueuse/core'
import { shallowRef } from 'vue'

const counter = shallowRef(0)
const { history, commit, undo, redo } = useManualRefHistory(counter)

counter.value += 1
commit()

console.log(history.value)
/* [
  { snapshot: 1, timestamp: 1601912898062 },
  { snapshot: 0, timestamp: 1601912898061 }
] */

你可以使用 undo 将 ref 的值重置到上一个历史记录点。

console.log(counter.value) // 1
undo()
console.log(counter.value) // 0

可变对象的历史

如果你要改变源对象,你需要传入一个自定义的克隆函数,或者使用参数 clone: true。clone: true 是一个快捷方式,它会使用一个最小化的克隆函数 x => JSON.parse(JSON.stringify(x)),这个函数将同时用于 dump 和 parse。

import { useManualRefHistory } from '@vueuse/core'
import { ref } from 'vue'

const counter = ref({ foo: 1, bar: 2 })
const { history, commit, undo, redo } = useManualRefHistory(counter, { clone: true })

counter.value.foo += 1
commit()

自定义克隆函数

要使用功能齐全或自定义的克隆函数,你可以通过 clone 选项进行设置。

例如,使用 structuredClone

import { useManualRefHistory } from '@vueuse/core'

const refHistory = useManualRefHistory(target, { clone: structuredClone })

或者使用 Lodash 的 cloneDeep

import { useManualRefHistory } from '@vueuse/core'
import { cloneDeep } from 'lodash-es'

const refHistory = useManualRefHistory(target, { clone: cloneDeep })

或者使用更轻量的 klona

import { useManualRefHistory } from '@vueuse/core'
import { klona } from 'klona'

const refHistory = useManualRefHistory(target, { clone: klona })

自定义 Dump 和 Parse 函数

除了使用 clone 选项,你还可以传入自定义函数来控制序列化和解析。如果你不需要历史值是对象,这可以在撤销时节省一次额外的克隆操作。当你想将快照直接字符串化以便保存到本地存储时,这也很有用。

import { useManualRefHistory } from '@vueuse/core'

const refHistory = useManualRefHistory(target, {
  dump: JSON.stringify,
  parse: JSON.parse,
})

历史容量

我们默认会保留所有历史记录(无限制),直到你明确清除它们。你也可以通过 capacity 选项来设置要保留的最大历史记录数量。

const refHistory = useManualRefHistory(target, {
  capacity: 15, // limit to 15 history records
})

refHistory.clear() // explicitly clear all the history

类型声明

export interface UseRefHistoryRecord<T> {
  snapshot: T // 存储着被追踪 ref 在某个时间点的值。T 是快照的数据类型
  timestamp: number // 记录该快照被创建时的 Unix 时间戳(毫秒)
}

export interface UseManualRefHistoryOptions<Raw, Serialized = Raw> {
  /**
   * Maximum number of history to be kept. Default to unlimited.
   */
  capacity?: number
  /**
   * Clone when taking a snapshot, shortcut for dump: JSON.parse(JSON.stringify(value)).
   * Default to false
   *
   * @default false
   */
  clone?: boolean | CloneFn<Raw>
  /**
   * Serialize data into the history
   */
  dump?: (v: Raw) => Serialized
  /**
   * Deserialize data from the history
   */
  parse?: (v: Serialized) => Raw
  /**
   * set data source
   */
  setSource?: (source: Ref<Raw>, v: Raw) => void
}

export interface UseManualRefHistoryReturn<Raw, Serialized> {
  /**
   * Bypassed tracking ref from the argument
   */
  source: Ref<Raw>
  /**
   * An array of history records for undo, newest comes to first
   */
  history: Ref<UseRefHistoryRecord<Serialized>[]>
  /**
   * Last history point, source can be different if paused
   */
  last: Ref<UseRefHistoryRecord<Serialized>>
  /**
   * Same as {@link UseManualRefHistoryReturn.history | history}
   */
  undoStack: Ref<UseRefHistoryRecord<Serialized>[]>
  /**
   * Records array for redo
   */
  redoStack: Ref<UseRefHistoryRecord<Serialized>[]>
  /**
   * A ref representing if undo is possible (non empty undoStack)
   */
  canUndo: ComputedRef<boolean>
  /**
   * A ref representing if redo is possible (non empty redoStack)
   */
  canRedo: ComputedRef<boolean>
  /**
   * Undo changes
   */
  undo: () => void
  /**
   * Redo changes
   */
  redo: () => void
  /**
   * Clear all the history
   */
  clear: () => void
  /**
   * Create a new history record
   */
  commit: () => void
  /**
   * Reset ref's value with latest history
   */
  reset: () => void
}

/**
 * 追踪一个 ref 的更改历史,并提供撤销(undo)和重做(redo)功能。
 *
 * @see https://vueuse.org/useManualRefHistory
 * @param source
 * @param options
 */
export declare function useManualRefHistory<Raw, Serialized = Raw>(
  source: Ref<Raw>,
  options?: UseManualRefHistoryOptions<Raw, Serialized>,
): UseManualRefHistoryReturn<Raw, Serialized>