1.3 State -- useRefHistory

1.3 State – useRefHistory

作用

追踪一个ref的变化历史,同时提供了undoredo方法。

这个方法就是useManualRefHistory的封装,不需要手动的commit,而是方法自动判断什么时候应该commit。因此我们需要搞清楚什么情况下,这个方法会自动提交commit

官方示例

  • 基础类型(数字、字符串等)
import { ref } from 'vue'
import { useRefHistory } from '@vueuse/core'

const counter = ref(0)

// 使用 useRefHistory 追踪 counter 的变化
// undo 撤销,redo 重做
const { history, undo, redo } = useRefHistory(counter)

在内部,watch用于在ref值被修改时触发历史点。这意味着历史点是在同一个“tick”中异步触发批量修改的。所以需要nextTick之后才能记录到变更。

counter.value += 1

await nextTick()
console.log(history.value)
/* [
  { snapshot: 1, timestamp: 1601912898062 },
  { snapshot: 0, timestamp: 1601912898061 }
] */
  • 对象/数组类型

在使用对象或数组时,由于更改它们的属性不会更改引用,因此不会触发提交。要跟踪属性的变化,需要传递deep: true。它将为每个历史记录创建克隆。

const state = ref({
  foo: 1,
  bar: 'bar',
})

// 使用 deep: true 来追踪对象属性变化
const { history, undo, redo } = useRefHistory(state, {
  deep: true,
})

state.value.foo = 2

await nextTick()
console.log(history.value)
/* [
  { snapshot: { foo: 2, bar: 'bar' } },
  { snapshot: { foo: 1, bar: 'bar' } }
] */
  • 函数

在内部,使用了简单的方式克隆函数,也就是x => JSON.parse(JSON.stringify(x))。通常来说,我们需要自己定义这个克隆的方式,比如最常用的,使用lodashcloneDeep函数

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

const refHistory = useRefHistory(target, { clone: cloneDeep })
  • 历史记录的容量
// 使用 capacity 来指定
const refHistory = useRefHistory(target, {
  capacity: 15, // limit to 15 history records
})

refHistory.clear() // explicitly clear all the history
  • 刷新的时机

vue的响应式系统会把一系列更新缓存起来,在合适的时机统一调用,以避免在同一个追踪中发生多个状态突变时不必要的重复调用。比如在同一个computed中,多个变量发生了变化,computed函数也只会调用一次。

watch一样,你可以指定flush选项来改变刷新时机。默认是pre

  1. 默认情况下,侦听器回调会在父组件更新 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。
  2. 同步触发的侦听器,它会在 Vue 进行任何更新之前触发。同步侦听器不会进行批处理,每当检测到响应式数据发生变化时就会触发。
  3. 如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: 'post' 选项:
const refHistory = useRefHistory(target, {
  flush: 'sync', // options 'pre' (default), 'post' and 'sync'
})

如果需要在同一次变更过程中,需要创建多个变更点,可以使用commit方法。

const r = ref(0)
const { history, commit } = useRefHistory(r)

r.value = 1
// 每一次commit,增加一次记录
commit()

r.value = 2
commit()

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

相反的,如果使用了 flush:'sync',你能够使用batch(fn)来为多个同步操作生成一个记录。

const r = ref({ names: [], version: 1 })
const { history, batch } = useRefHistory(r, { flush: 'sync' })

// 因为是sync模式,所以两次操作应该生成两条记录,用batch可以合并记录
batch(() => {
  r.value.names.push('Lena')
  r.value.version++
})

console.log(history.value)
/* [
  { snapshot: { names: [ 'Lena' ], version: 2 },
  { snapshot: { names: [], version: 1 },
] */

对于splice操作,默认是要生成三条操作记录的。如果使用了{ flush: 'sync', deep: true },也可使用batch(fn)来合并这些操作为一条记录。

const arr = ref([1, 2, 3])
const { history, batch } = useRefHistory(arr, { deep: true, flush: 'sync' })

// 必须把操作包裹在 batch 中
batch(() => {
  arr.value.splice(1, 1) 
})

源码分析

// 先举一个例子,和useManualRefHistory一样
假设 source 的变化为: 1 --> 2 --> 3

那么: undoStack     [] --> [1] --> [2, 1] // 两个数组都是从前面放,从前面取
			redoStack     [] --> [] --> []

执行 undo 操作后,
			undoStack     [] --> [1] --> [2, 1] --> [1]
			redoStack     [] --> []  --> []     --> [2]

再次执行 undo 操作后,
			undoStack     [] --> [1] --> [2, 1] --> [1] --> []
			redoStack     [] --> []  --> []     --> [2] --> [1, 2]

执行 redo 操作后,
			undoStack     [] --> [1] --> [2, 1] --> [1] --> []     --> [1]
			redoStack     [] --> []  --> []     --> [2] --> [1, 2] --> [2]

先看一下useRefhistory的实现,在packages/core/useRefhistory/index.ts文件中。源码太多,这里不贴了,地址如下。

https://github.com/vueuse/vueuse/blob/main/packages/core/useRefHistory/index.ts

1、首先看一下pauseresume是怎么实现的(这两个方法是手动版本没有的,因为手动的可以完全控制追踪时机)。

这段代码返回了一个对象,实现了一个可以暂停和恢复的事件过滤器。可以通过pause来暂停跟踪过程,resume来恢复跟踪。

const {
    eventFilter: composedFilter,
    pause,
    resume: resumeTracking,
    isActive: isTracking,
  } = pausableFilter(eventFilter)

内部实现非常简单,其中bypassFilter就是一个直接执行函数。通过修改isActive来控制回调是否执行。

export function pausableFilter(extendFilter: EventFilter = bypassFilter): Pausable & { eventFilter: EventFilter } {
  const isActive = ref(true)

  function pause() {
    isActive.value = false
  }
  function resume() {
    isActive.value = true
  }

  const eventFilter: EventFilter = (...args) => {
    if (isActive.value)
      extendFilter(...args)
  }

  return { isActive, pause, resume, eventFilter }
}

2、自动监控是如何实现的。

watchIgnorable这个函数扩展了watch函数的功能,以便能够更精细地控制何时忽略数据源(source)的更新。这个功能在处理撤销/重做、数据回滚或任何需要基于历史操作动态忽略某些状态更新的场景中非常有用。

const {
  ignoreUpdates,
  ignorePrevAsyncUpdates,
  stop,
} = watchIgnorable(
  source,
  commit,
  { deep, flush, eventFilter: composedFilter },
)

watchIgnorable的实现在 https://github.com/vueuse/vueuse/blob/main/packages/shared/watchIgnorable/index.ts

这个函数接受三个参数,第一个是监控的源数据source,用来被watch监控;第二个是cb,也就是手动版本的commit方法,用来作为回调执行(cb应该是ignorePrevAsyncUpdates()+manualCommit(),为了理解方便,就当作manualCommit吧);第三个是配置options,直接作为watch的第三个参数,包括flushdeep等参数。

export function watchIgnorable<Immediate extends Readonly<boolean> = false>(
  source: any,
  cb: any,
  options: WatchWithFilterOptions<Immediate> = {},
): WatchIgnorableReturn {
  const {
    eventFilter = bypassFilter,
    ...watchOptions
  } = options

  const filteredCb = createFilterWrapper(
    eventFilter,
    cb,
  )

  let ignoreUpdates: IgnoredUpdater
  let ignorePrevAsyncUpdates: () => void
  let stop: () => void

  // ......
  

  return { stop, ignoreUpdates, ignorePrevAsyncUpdates }
}

具体的执行过程分为两种,

  • 第一种是同步模式。顾名思义,同步模式下,每一次修改source,都会commit一次,生成一次记录。
if (watchOptions.flush === 'sync') {
    const ignore = ref(false)

    // 由于flush模式被设置为'sync',这意味着所有的更新都是同步的,因此没有必要(也无法)忽略之前的异步更新。
    ignorePrevAsyncUpdates = () => {}

    ignoreUpdates = (updater: () => void) => {
      // 由于flush模式为'sync',这个忽略机制实际上并不直接“计数”或“忽略”任何更新,
      // 而是通过条件检查(在watch的回调函数中)来避免在ignore.value为true时调用filteredCb。
      ignore.value = true
      updater()
      ignore.value = false
    }

  	// 用户会调用ignoreUpdates,传递一个方法updater去执行,假如这个方法执行很快(假设是同步方法),
  	// 那么watch触发的时候,ignore就一直是false。
  	// 也就是说同步模式下,每次source更新,都会commit一个数据。
    stop = watch(
      source,
      (...args) => {
        if (!ignore.value)
          filteredCb(...args)
      },
      watchOptions,
    )
  }
  • 第二种是异步模式,包括prepost模式
else {
    // flush 'pre' and 'post'
    const disposables: Fn[] = []

    /**
    * 这两个变量的用途:如果同步触发次数多于忽略计数,那么我们现在需要提交ref值中的修改。
    * 
    * 在发生影响ref的历史操作(撤销、重做、恢复)之前,将递增ignoreCounter(普通的修改不递增ignoreCounter)。
    * syncCounter随着ref值的每次更改同步递增。这让我们知道ref修改了多少次,并支持链式同步操作。
    */
    const ignoreCounter = ref(0)
    const syncCounter = ref(0)

    // 忽略之前的异步更新,方法就是让ignoreCounter和syncCounter相等
    // 因为在下面第二个watch中,如果ignore,就不会触发commit了
    ignorePrevAsyncUpdates = () => {
      ignoreCounter.value = syncCounter.value
    }

    // 使用同步的方式,统计ref修改了多少次
    disposables.push(
      watch(
        source,
        () => {
          syncCounter.value++
        },
        { ...watchOptions, flush: 'sync' },
      ),
    )

    ignoreUpdates = (updater: () => void) => {
      /**
      * 这个方法是干什么的?暂时先不管,我们先看它的作用
      *
      * 执行updater函数,并且记录一下在执行过程中执行了多少同步修改,
      * 把这个值加到忽略的次数上
      *
      * 比如updater()之前,syncCounter和ignoreCounter都是0
      * updater()修改了两次source,那么syncCounter就是2,ignoreCounter也是2
      */
      const syncCounterPrev = syncCounter.value
      updater()
      ignoreCounter.value += syncCounter.value - syncCounterPrev
    }

  	/**
  	* ignoreCounter 记录了历史操作的数量。如果操作的数量ignoreCounter === syncCounter,也就是说source没有其他更新。
  	* 因为每一次历史操作,也会让syncCounter加1,所以二者相等就代表没有其他修改。
  	*
  	* 如果没有其他修改,则忽略此次提交;
  	* 如果不是历史操作,而是其他修改,比如soucr++,ignoreCounter 一定比syncCounter少1,那么这时候否则触发一次commit。
  	*
    * 重置ignoreCounter和syncCounter,为下一次更新做准备
  	*/
    disposables.push(
      watch(
        source,
        (...args) => {
          // 如果执行了历史操作(ignoremter > 0)并且之后没有对源ref值进行其他更改,则忽略此提交
          const ignore = ignoreCounter.value > 0 && ignoreCounter.value === syncCounter.value
          ignoreCounter.value = 0
          syncCounter.value = 0
          if (ignore)
            return

          filteredCb(...args)
        },
        watchOptions,
      ),
    )
		
  	// disposables保存了watch的销毁函数,依次执行,销毁所有watch
    stop = () => {
      disposables.forEach(fn => fn())
    }
  }

3、使用了useManualRefHistory生成一个手动的版本,把commit方法抛出来,给自动的hook使用。

  const manualHistory = useManualRefHistory(source, { ...options, clone: options.clone || deep, setSource })

  const { clear, commit: manualCommit } = manualHistory

4、接下来看一下这个hook特有的方法

/**
* 调用ignorePrevAsyncUpdates,让ignoreCounter和syncCounter保持一致,目的是不使用自动更新;
* 然后调用一次手动更新方法
*/
function commit() {
  // 只有pre 和 post 下管用,因为sync 模式下,这个函数是空函数
  ignorePrevAsyncUpdates()

  manualCommit()
}

/**
* 配合batch的使用,以及ignoreUpdates这个函数来看
* batch(() => {
*   r.value.names.push('Lena')
*   r.value.version++
* })
* ignoreUpdates = (updater: () => void) => {
*     const syncCounterPrev = syncCounter.value
*     updater()
*     ignoreCounter.value += syncCounter.value - syncCounterPrev
* }
* 
*/
function batch(fn: (cancel: Fn) => void) {
  let canceled = false

  const cancel = () => canceled = true

  // 结合上面的函数可以看出,cancel并没有被执行,也就是canceled始终为false
  // 从这个实现来看,ignoreUpdates并没有起到作用。除非在执行fn过程中,手动执行cancel
  ignoreUpdates(() => {
    fn(cancel)
  })

  // canceled始终为false,也就是在这个hook中,这段是一定会执行的。调用commit,依然是不自动更新,触发手动更新
  if (!canceled)
    commit()
}

以上过程解释了代码是如何运作的,但是对于一些细节还是描述不够,比如为什么ignoreCounter代表了历史操作的次数?如果没有历史操作,自动更新的流程是什么?我们通过两个例子来说明。

1、修改ref的值,只会触发两个watch,且同步的watch先触发

在这里插入图片描述

2、执行undo之后,数据的变化如下
在这里插入图片描述

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
kube-state-metrics是一个开源的Kubernetes组件,用于将Kubernetes资源信息转换为指标(metrics)并暴露给Prometheus。YAML是Kubernetes中常用的声明式配置文件格式。kube-state-metrics也需要通过YAML配置文件进行部署和管理。下面来详细介绍kube-state-metrics YAML的几个方面。 首先,kube-state-metrics YAML需要配置Kubernetes API Server的地址、证书等信息,以便通过API Server获取资源信息。其次,kube-state-metrics需要指定需要收集的资源类型,比如Pod、Deployment等。这些资源类型通过Kubernetes API定义,kube-state-metrics会通过API Server获取这些资源的详细信息,并将其转换为指标。此外,kube-state-metrics YAML还需要指定需要暴露给Prometheus的指标端口等信息,以便Prometheus可以通过这些指标监控Kubernetes集群各种资源的使用情况。 在kube-state-metrics的配置中,还需要指定各个资源指标的定义方式,例如当Pod或Deployment出现异常时,kube-state-metrics需要如何定义这些指标?以及在Prometheus中如何查询这些指标。因此,可以在kube-state-metrics的YAML文件中定义各个指标的名称,以及在Prometheus中的查询方式。这里的细节需要参考kube-state-metrics的文档。 总之,kube-state-metrics YAML是配置kube-state-metrics组件的一种方法,通过对该文件进行配置,可以定制化kube-state-metrics组件的行为,并将它整合到Kubernetes集群中。而kube-state-metrics的指标可以用于Kubernetes集群的监控和自动化管理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值