likes
comments
collection
share

写给自己的 vueuse 学习文章(第一卷)

作者站长头像
站长
· 阅读数 45

人们因能感知太多信息而感到心神不宁,或因产生过多欲望而痛苦不堪,又或因担忧能力不足而滋生焦虑,无论顺境或是逆境都不得安生。就像今天的我们,虽衣食无忧,却总是苦于无法摆脱手机的干扰,无法获取让人羡慕的技能,无法拥有想要的生活,等等。低层次的动物是没有这种烦恼的,它们的心灵只容纳环境中确实存在的、与它们切身相关的、靠直觉判断的信息——饥饿的狮子只注意能帮助它猎到羚羊的信息,吃饱的狮子的注意力则集中在温暖的阳光上...... ——《认知觉醒》

前言

本文章在于理解熟练State菜单下的所有相关hooks,之所以写这篇文章,只是想要更多的pr,输出即是最好的学习。

State

在State下目前共有15hooks提供使用:

1. createGlobalState

保持全局范围的数据状态,使其可以在任意vue实例中使用

想知道他如何使用,以及使用他有什么样的功能,最好的方式就是查看单元测试

import { computed, ref } from 'vue-demi'  
import { describe, expect, it } from 'vitest'  
import { useSetup } from '../../.test'  
import { createGlobalState } from '.'  
  
describe('createGlobalState', () => {  
  it('should work after dispose 1', async () => {  
    const useGlobalState = createGlobalState(() => {  
      const counter = ref(1)  
      const doubled = computed(() => counter.value * 2)  
  
      return {  
        counter,  
        doubled,  
      }    
    })  
    const { counter, doubled } = useGlobalState()  
  
    const vm = useSetup(() => {  
      const { counter, doubled } = useGlobalState()  
  
      expect(counter.value).toBe(1)  
      expect(doubled.value).toBe(2)  
  
      return {  
        counter,  
        doubled,  
      }    
    })  
    counter.value = 2  
  
    expect(counter.value).toBe(2)  
    expect(doubled.value).toBe(4)  
  
    vm.unmount()  
  
    counter.value = 3  
    expect(counter.value).toBe(3)  
    expect(doubled.value).toBe(6)  
  })})

看完这个单侧,大致可以得到的信息:

  • 使用 createGlobalState 函数会返回一个函数useGlobalState
  • 执行useGlobalState函数,会返回createGlobalState函数的参数的返回值
  • createGLobalState的参数函数里的返回值是全局的

接下来,看看它的实现

import { effectScope } from 'vue-demi'  
import type { AnyFn } from '../utils'  
  
/**  
 * Keep states in the global scope to be reusable across Vue instances. * * @see https://vueuse.org/createGlobalState  
 * @param stateFactory A factory function to create the state  
 */
export function createGlobalState<Fn extends AnyFn>(  
  stateFactory: Fn,  
): Fn {  
  let initialized = false  
  let state: any  
  const scope = effectScope(true)  
  
  return ((...args: any[]) => {  
    if (!initialized) {  
      state = scope.run(() => stateFactory(...args))!  
      initialized = true  
    }  
    return state  
  }) as Fn  
}
  • createGlobalState 接受一个函数作为参数
  • 内部创建一个 scope,也就是创建了单独的作用域
  • 里面 return 一个函数,就是 useGlobalState 函数,当它执行后,会判断是否是初始化,如果是初始化,那就直接返回独立作用域中已有的 state,如果不是,那就执行 run 创建,也就是执行 createGlobalState 的参数函数

2.createInjectionState

创建一个可以注入到组件的全局状态

要想看如何使用,还是要看单测,该文件单测过长,只挑一部分看,感兴趣的可以去这里查看完整的: createInjectionState

it('should work for simple nested component', async () => {  
  const [useProvideCountState, useCountState] = createInjectionState((initialValue: number) => {  
    const count = ref(initialValue)  
    return count  
  })  
  
  let count: Ref<number> | undefined  
  
  const ChildComponent = defineComponent({  
    setup() {  
      count = useCountState()  
  
      return () => h('div')  
    },  })  
  const RootComponent = defineComponent({  
    setup() {  
      useProvideCountState(114514)  
  
      return () => h(ChildComponent)  
    },  })  
  const vm = mount(RootComponent)  
  await nextTick()  
  
  expect(count?.value).toBe(114514)  
  vm.unmount()  
})
  • createInjectionState 参数是一个函数,函数的返回值就是提供给后代组建使用的值
  • createInjectionState 返回一个数组,数组有两个 hooks,分别是 useProvideCountState 和 useCountState
  • useProvideCountState 中的参数,就是 initialValue 的初始值
  • useCountState 的返回值就是 return count
  • useProvideCountState 在父级组件中使用,提供数据,在后代组件中使用 useCountState 注入提供的数据使用

当然要想知道它的细节实现,还得看源码(防止看的头晕,删除了类型)

import type { InjectionKey } from 'vue-demi'  
import { provideLocal } from '../provideLocal'  
import { injectLocal } from '../injectLocal'  
  
export interface CreateInjectionStateOptions<Return> {  
  /**  
   * Custom injectionKey for InjectionState   
   */  
   injectionKey?: string | InjectionKey<Return>  
  /*
   * Default value for the InjectionState  
   */  
   defaultValue?: Return  
}  
  
/**  
 * Create global state that can be injected into components. * * @see https://vueuse.org/createInjectionState  
 *  
 */
 export function createInjectionState(  
  composable,  
  options,  
){  
  const key = options?.injectionKey || Symbol(composable.name || 'InjectionState')  
  const defaultValue = options?.defaultValue  
  const useProvidingState = (...args: Arguments) => {  
    const state = composable(...args)  
    provideLocal(key, state)  
    return state  
  }  
  const useInjectedState = () => injectLocal(key, defaultValue)  
  return [useProvidingState, useInjectedState]  
}
  • 可以看到,createInjectionState 是可以接收两个参数,一个函数,一个options,当 options 拥有 defaultValue 值的时候,它会注入一个数据在当前组件,并赋有默认值 defaultValue
  • 内部可以看到 useProvidingState 函数的接收值就是传值给了 createInjectionState 接收的第一个参数函数的参数了,也就是前面提到的 initialValue
  • 这里重要的代码其实就是 provideLocal 和 injectLocal 两个函数,大致就是 provide 和 inject 效果,详细代码可以查看:injectLocal provideLocal

3. createSharedComposable

创建一个可在多个 Vue 实例中使用的可组合函数。

没有单测,看源码吧

import type { EffectScope } from 'vue-demi'  
import { effectScope } from 'vue-demi'  
import { tryOnScopeDispose } from '../tryOnScopeDispose'  
import type { AnyFn } from '../utils'  
  
/**  
 * Make a composable function usable with multiple Vue instances. * * @see https://vueuse.org/createSharedComposable  
 */  
export function createSharedComposable<Fn extends AnyFn>(composable: Fn): Fn {  
  let subscribers = 0  
  let state: ReturnType<Fn> | undefined  
  let scope: EffectScope | undefined  
  
  const dispose = () => {  
    subscribers -= 1  
    if (scope && subscribers <= 0) {  
      scope.stop()  
      state = undefined  
      scope = undefined  
    }  
  }  
  return <Fn>((...args) => {  
    subscribers += 1  
    if (!state) {  
      scope = effectScope(true)  
      state = scope.run(() => composable(...args))  
    }    
    tryOnScopeDispose(dispose)  
    return state  
  })  
}
  • tryOnScopeDispose 当你组件不在需要时,执行清理所有副作用
  • createSharedComposable 接收一个函数,并且返回一个函数,且使用必报方式,让初始化只有一次,后续调用函数,都是共用一个 state

文档的基础使用方式:

import { createSharedComposable, useMouse } from '@vueuse/core' 
const useSharedMouse = createSharedComposable(useMouse) 
// CompA.vue 
const { x, y } = useSharedMouse() 
// CompB.vue - 将重用先前的状态,不会注册新的事件监听器 
const { x, y } = useSharedMouse()

x,y是 useMouse 返回的值。

4.injectLocal & provideLocal

扩展了 inject,能够调用 provideLocal 在同一组件中提供值 扩展了 provide,能够使其通过调用 injectLocal 在同一组件中获取值

单测:

it('should work for nested component', async () => {  
  const CountKey: InjectionKey<number> | string = Symbol('count')  
  let count: number | undefined  
  const ChildComponent = defineComponent({  
    setup() {  
      count = injectLocal(CountKey)  
  
      return () => h('div')  
    },  
  })  
  const RootComponent = defineComponent({  
    setup() {  
      provideLocal(CountKey, 2333)  
  
      return () => h(ChildComponent)  
    },
  })  
  const vm = mount(RootComponent)  
  await nextTick()  
  
  expect(count).toBe(2333)  
  vm.unmount()  
})
  • 只是扩展了 provide 和 inject 的功能,但是使用方式上还是相同的 查看源码
// injectLocal

import { getCurrentInstance, inject } from 'vue-demi'  
import { localProvidedStateMap } from '../provideLocal/map'  
  
/**  
 * On the basis of `inject`, it is allowed to directly call inject to obtain the value after call provide in the same component. * 
 * @example  
 * ```ts  
 * injectLocal('MyInjectionKey', 1) * const injectedValue = injectLocal('MyInjectionKey') // injectedValue === 1 
 * ``` 
 */
 // @ts-expect-error overloads are not compatible  
export const injectLocal: typeof inject = (...args) => {  
  const key = args[0] as string | symbol  
  const instance = getCurrentInstance()?.proxy  
  if (instance == null)  
    throw new Error('injectLocal must be called in setup')  
  
  if (localProvidedStateMap.has(instance) && key in localProvidedStateMap.get(instance)!)  
    return localProvidedStateMap.get(instance)![key]  
  
  // @ts-expect-error overloads are not compatible  
  return inject(...args)  
}
  • vueuse 自己维护了一个StateMap,当传入一个key值的时候,且key值是在当前组件实例中的属性时,直接返回内部维护的值,如果不是通过 vueuse 设置的值,不在 StateMap 中,那就返回 inject 内的值
import { getCurrentInstance, provide } from 'vue-demi'  
import { localProvidedStateMap } from './map'  
  
/**  
 * On the basis of `provide`, it is allowed to directly call inject to obtain the value after call provide in the same component. * * @example  
 * ```ts  
 * provideLocal('MyInjectionKey', 1) 
 * const injectedValue = injectLocal('MyInjectionKey') 
 * // injectedValue === 1 * ``` 
 */
 export const provideLocal: typeof provide = (key, value) => {  
  const instance = getCurrentInstance()?.proxy  
  if (instance == null)  
    throw new Error('provideLocal must be called in setup')  
  
  if (!localProvidedStateMap.has(instance))  
    localProvidedStateMap.set(instance, Object.create(null))  
  
  const localProvidedState = localProvidedStateMap.get(instance)!  
  // @ts-expect-error allow InjectionKey as key  
  localProvidedState[key] = value  
  provide(key, value)  
}
  • 在内部维护了一个StateMap,当使用这个api进行provide的时候,先存一份在 StateMap 中,再继续使用 provide 存储在组件实例上

5.useAsyncState

响应式同步状态

单元测试(一部分)

import { promiseTimeout } from '@vueuse/shared'  
import { describe, expect, it, vi } from 'vitest'  
import { useAsyncState } from '.'  
  
describe('useAsyncState', () => {  
  it('should be defined', () => {  
    expect(useAsyncState).toBeDefined()  
  })
    
  const p1 = (num = 1) => {  
    return new Promise((resolve) => {  
      setTimeout(() => {  
        resolve(num)  
      }, 50)  
    })  
  }
    
  const p2 = async (id?: string) => {  
    if (!id)  
      throw new Error('error')  
    return id  
  } 
  
  it('should work', async () => {  
    const { execute, state } = useAsyncState(p1, 0)  
    expect(state.value).toBe(0)  
    await execute(0, 2)  
    expect(state.value).toBe(2)  
  })
  
  it('should work with onError', async () => {  
    const onError = vi.fn()  
    const { execute } = useAsyncState(p2, '0', { onError, immediate: false })  
    await execute()  
    expect(onError).toHaveBeenCalled()  
    expect(onError).toHaveBeenCalledWith(new Error('error'))  
  })
})
  • useAsyncState拥有两个参数,第一个参数可以是一个返回Promise的函数,第二个参数是一个值,这个值真是第一个参数函数中的参数的初始值
  • useAsyncState返回值是一个对象,对象里面有execute函数和state,state的初始值,就是useAsyncState的第二个值,通过execute异步修改state值,state的值也更新了

详细实现还需要看源码实现(删除了类型实现,代码太多):

import { noop, promiseTimeout, until } from '@vueuse/shared'  
import type { Ref, UnwrapRef } from 'vue-demi'  
import { ref, shallowRef } from 'vue-demi'  
  
export function useAsyncState<Data, Params extends any[] = [], Shallow extends boolean = true>(  
  promise: Promise<Data> | ((...args: Params) => Promise<Data>),  
  initialState: Data,  
  options?: UseAsyncStateOptions<Shallow, Data>,  
): UseAsyncStateReturn<Data, Params, Shallow> {  
  const {  
    immediate = true,  
    delay = 0,  
    onError = noop,  
    onSuccess = noop,  
    resetOnExecute = true,  
    shallow = true,  
    throwError,  
  } = options ?? {}  
  const state = shallow ? shallowRef(initialState) : ref(initialState)  
  const isReady = ref(false)  
  const isLoading = ref(false)  
  const error = shallowRef<unknown | undefined>(undefined)  
  
  async function execute(delay = 0, ...args: any[]) {  
    if (resetOnExecute)  
    state.value = initialState  
    error.value = undefined  
    isReady.value = false  
    isLoading.value = true  
  
    if (delay > 0)  
      await promiseTimeout(delay)  
  
    const _promise = typeof promise === 'function'  
      ? promise(...args as Params)  
      : promise  
  
    try {  
      const data = await _promise  
      state.value = data  
      isReady.value = true  
      onSuccess(data)  
    } catch (e) {  
      error.value = e  
      onError(e)  
      if (throwError)  
        throw e  
    }  
    finally {  
      isLoading.value = false  
    }  
  
    return state.value as Data  
  }  
  
  if (immediate)  
    execute(delay)  
  
  const shell: UseAsyncStateReturnBase<Data, Params, Shallow> = {  
    state: state as Shallow extends true ? Ref<Data> : Ref<UnwrapRef<Data>>,  
    isReady,  
    isLoading,  
    error,  
    execute,  
  }  
  function waitUntilIsLoaded() {  
    return new Promise<UseAsyncStateReturnBase<Data, Params, Shallow>>((resolve, reject) => {  
      until(isLoading).toBe(false)  
        .then(() => resolve(shell))  
        .catch(reject)  
    })  
  }  
  return {  
    ...shell,  
    then(onFulfilled, onRejected) {  
      return waitUntilIsLoaded()  
        .then(onFulfilled, onRejected)  
    },  
}}
  • useAsyncState 接收3个参数,分别是promise,initialState,options
  • options中可以设置shallow,用于设置state是使用shallowRef或者ref
  • useAsyncState返回了state, isReady, isLoading, error, execute和then函数,且then函数成功后,拿到的数据 resolve({state, isReady, isLoading, error, execute})
  • execute函数接收的第一个参数是delay,当它大于0的时候进行await等待promise返回,最后将用户传入的promise的返回值复制给state,onSuccess是通过options传递进来的函数,promise成功后,会调用onSuccess,将返回值通过 onSuccess返回一份

6.useDebouncedRefHistory

具有防抖过滤器的useRefHistory的简写

单测

import { nextTick, ref } from 'vue-demi'  
import { promiseTimeout } from '@vueuse/shared'  
import { describe, expect, it } from 'vitest'  
import { useDebouncedRefHistory } from '.'  
  
describe('useDebouncedRefHistory', () => {  
  it('once the ref\'s value has changed and some time has passed, ensure the snapshot is updated', async () => {  
    const v = ref(0)  
  
    const { history } = useDebouncedRefHistory(v, { debounce: 10 })  
    v.value = 100  
    expect(history.value.length).toBe(1)  
    expect(history.value[0].snapshot).toBe(0)  
  
    await promiseTimeout(20)  
  
    expect(history.value.length).toBe(2)  
    expect(history.value[0].snapshot).toBe(100)  
  })  
  it('when debounce is undefined', async () => {  
    const v = ref(0)  
  
    const { history } = useDebouncedRefHistory(v, { deep: false })  
  
    v.value = 100  
  
    await nextTick()  
  
    expect(history.value.length).toBe(2)  
    expect(history.value[0].snapshot).toBe(100)  
  })
})
  • useDebounceRefHistory 接收两个参数,一个是源头值,一个是配置options
  • debounce设置毫秒,当下一次操作与上一次操作的时间超过debounce时,才记录历史 详细实现
import type { MaybeRefOrGetter } from '@vueuse/shared'  
import { debounceFilter } from '@vueuse/shared'  
import type { Ref } from 'vue-demi'  
import type { UseRefHistoryOptions, UseRefHistoryReturn } from '../useRefHistory'  
import { useRefHistory } from '../useRefHistory'  
  
/**  
 * Shorthand for [useRefHistory](https://vueuse.org/useRefHistory) with debounce filter.  
 * * @see https://vueuse.org/useDebouncedRefHistory  
 * @param source  
 * @param options  
 */  
export function useDebouncedRefHistory<Raw, Serialized = Raw>(  
  source: Ref<Raw>,  
  options: Omit<UseRefHistoryOptions<Raw, Serialized>, 'eventFilter'> & { debounce?: MaybeRefOrGetter<number> } = {},  
): UseRefHistoryReturn<Raw, Serialized> {  
  const filter = options.debounce ? debounceFilter(options.debounce) : undefined  
  const history = useRefHistory(source, { ...options, eventFilter: filter })  
  
  return {  
    ...history,  
  }}
  • debounceFilter 大致就是判断是否创建了定时器,如果创建了定时器,直接清除,重新创建,如果没有创建,那就创建一个定时器,当到达时间后会执行一个 resolve
  • useRefHistory 跟踪 ref 的变化历史,还提供了撤销和重做功能。

7.useLastChanged

记录最后一次改变的时间戳

改 hooks 没有单测,直接看实现

import type { Ref, WatchOptions, WatchSource } from 'vue-demi'  
import { ref, watch } from 'vue-demi'  
import { timestamp } from '../utils'  
  
export interface UseLastChangedOptions<  
  Immediate extends boolean,  
  InitialValue extends number | null | undefined = undefined,  
> extends WatchOptions<Immediate> {  
  initialValue?: InitialValue  
}  
  
/**  
 * Records the timestamp of the last change * * @see https://vueuse.org/useLastChanged  
 */  
export function useLastChanged(source: WatchSource, options?: UseLastChangedOptions<false>): Ref<number | null>  
export function useLastChanged(source: WatchSource, options: UseLastChangedOptions<true> | UseLastChangedOptions<boolean, number>): Ref<number>  
export function useLastChanged(source: WatchSource, options: UseLastChangedOptions<boolean, any> = {}): Ref<number | null> | Ref<number> {  
  const ms = ref<number | null>(options.initialValue ?? null)  
  
  watch(  
    source,  
    () => ms.value = timestamp(),  
    options,  
  )  
  return ms  
}
  • 封装了一层 watch,options提供一个initialValue作为内部 ms 时间戳的初始值
  • 监听用户传的 source 数据,当source数据改变时,出发监听,同时修改 ms 的值为当前时间戳,最后返回了这个 ms ref对象

8.useLocalStorage & useSessionStorage

响应式LocalStorage & 响应式SessionStorage

该 hooks 没有单测,直接查看源码

import type { MaybeRefOrGetter, RemovableRef } from '@vueuse/shared'  
import type { UseStorageOptions } from '../useStorage'  
import { useStorage } from '../useStorage'  
import { defaultWindow } from '../_configurable'  
  
export function useLocalStorage(key: string, initialValue: MaybeRefOrGetter<string>, options?: UseStorageOptions<string>): RemovableRef<string>  
export function useLocalStorage(key: string, initialValue: MaybeRefOrGetter<boolean>, options?: UseStorageOptions<boolean>): RemovableRef<boolean>  
export function useLocalStorage(key: string, initialValue: MaybeRefOrGetter<number>, options?: UseStorageOptions<number>): RemovableRef<number>  
export function useLocalStorage<T>(key: string, initialValue: MaybeRefOrGetter<T>, options?: UseStorageOptions<T>): RemovableRef<T>  
export function useLocalStorage<T = unknown>(key: string, initialValue: MaybeRefOrGetter<null>, options?: UseStorageOptions<T>): RemovableRef<T>  
  
/**  
 * Reactive LocalStorage. * * @see https://vueuse.org/useLocalStorage  
 * @param key  
 * @param initialValue  
 * @param options  
 */  
export function useLocalStorage<T extends(string | number | boolean | object | null)>(  
  key: string,  
  initialValue: MaybeRefOrGetter<T>,  
  options: UseStorageOptions<T> = {},  
): RemovableRef<any> {  
  const { window = defaultWindow } = options  
  return useStorage(key, initialValue, window?.localStorage, options)  
}
  • 先不用管 useStorage 具体实现,大致能猜出,使用 key value存储,但是响应式的,useStorage 内部做了处理,useLocalStorage 就是在其记录上,限定了使用 localStorage

9.useManualRefHistory

手动跟踪 ref 的变化历史,当使用者调用 commit() 时,也提供了撤销和重做功能。

单元测试

import { isReactive, ref } from 'vue-demi'  
import { describe, expect, it } from 'vitest'  
import { useManualRefHistory } from '.'  
  
describe('useManualRefHistory', () => {  
  it('should record', () => {  
    const v = ref(0)  
    const { history, commit } = useManualRefHistory(v)  
  
    expect(history.value.length).toBe(1)  
    expect(history.value[0].snapshot).toBe(0)  
  
    v.value = 2  
    commit()  
  
    expect(history.value.length).toBe(2)  
    expect(history.value[0].snapshot).toBe(2)  
    expect(history.value[1].snapshot).toBe(0)  
  })  
  it('should be able to undo and redo', () => {  
    const v = ref(0)  
    const { commit, undo, redo, clear, canUndo, canRedo, history, last } = useManualRefHistory(v)  
  
    expect(canUndo.value).toBe(false)  
    expect(canRedo.value).toBe(false)  
  
    v.value = 2  
    commit()  
    v.value = 3  
    commit()  
    v.value = 4  
    commit()  
  
    expect(canUndo.value).toBe(true)  
    expect(canRedo.value).toBe(false)  
  
    expect(v.value).toBe(4)  
    expect(history.value.length).toBe(4)  
    expect(last.value.snapshot).toBe(4)  
    undo()  
  
    expect(canUndo.value).toBe(true)  
    expect(canRedo.value).toBe(true)  
  
    expect(v.value).toBe(3)  
    expect(last.value.snapshot).toBe(3)  
    undo()  
    expect(v.value).toBe(2)  
    expect(last.value.snapshot).toBe(2)  
    redo()  
    expect(v.value).toBe(3)  
    expect(last.value.snapshot).toBe(3)  
    redo()  
    expect(v.value).toBe(4)  
    expect(last.value.snapshot).toBe(4)  
  
    expect(canUndo.value).toBe(true)  
    expect(canRedo.value).toBe(false)  
  
    redo()  
    expect(v.value).toBe(4)  
    expect(last.value.snapshot).toBe(4)  
  
    clear()  
    expect(canUndo.value).toBe(false)  
    expect(canRedo.value).toBe(false)  
  })
})
  • useManualRefHistory 返回了 n 多个函数和ref对象
  • 目测好像所有的操作都是交给了用户,所有的函数,都是暴露给用户的操作

查看源码

import { timestamp } from '@vueuse/shared'  
import type { Ref } from 'vue-demi'  
import { computed, markRaw, ref } from 'vue-demi'  
import type { CloneFn } from '../useCloned'  
import { cloneFnJSON } from '../useCloned'  
  
export interface UseRefHistoryRecord<T> {  
  snapshot: T  
  timestamp: number  
}  
  
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: Ref<boolean>  
  
  /**  
   * A ref representing if redo is possible (non empty redoStack)   */
   canRedo: Ref<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  
}  
  
function fnBypass<F, T>(v: F) {  
  return v as unknown as T  
}  
function fnSetSource<F>(source: Ref<F>, value: F) {  
  return source.value = value  
}  
  
type FnCloneOrBypass<F, T> = (v: F) => T  
  
function defaultDump<R, S>(clone?: boolean | CloneFn<R>) {  
  return (clone  
    ? typeof clone === 'function'  
      ? clone  
      : cloneFnJSON  
    : fnBypass  
  ) as unknown as FnCloneOrBypass<R, S>  
}  
  
function defaultParse<R, S>(clone?: boolean | CloneFn<R>) {  
  return (clone  
    ? typeof clone === 'function'  
      ? clone  
      : cloneFnJSON  
    : fnBypass  
  ) as unknown as FnCloneOrBypass<S, R>  
}  
  
/**  
 * Track the change history of a ref, also provides undo and redo functionality. * * @see https://vueuse.org/useManualRefHistory  
 * @param source  
 * @param options  
 */  
export function useManualRefHistory<Raw, Serialized = Raw>(  
  source: Ref<Raw>,  
  options: UseManualRefHistoryOptions<Raw, Serialized> = {},  
): UseManualRefHistoryReturn<Raw, Serialized> {  
  const {  
    clone = false,  
    dump = defaultDump<Raw, Serialized>(clone),  
    parse = defaultParse<Raw, Serialized>(clone),  
    setSource = fnSetSource,  
  } = options  
  
  function _createHistoryRecord(): UseRefHistoryRecord<Serialized> {  
    return markRaw({  
      snapshot: dump(source.value),  
      timestamp: timestamp(),  
    })  }  
  const last: Ref<UseRefHistoryRecord<Serialized>> = ref(_createHistoryRecord()) as Ref<UseRefHistoryRecord<Serialized>>  
  
  const undoStack: Ref<UseRefHistoryRecord<Serialized>[]> = ref([])  
  const redoStack: Ref<UseRefHistoryRecord<Serialized>[]> = ref([])  
  
  const _setSource = (record: UseRefHistoryRecord<Serialized>) => {  
    setSource(source, parse(record.snapshot))  
    last.value = record  
  }  
  
  const commit = () => {  
    undoStack.value.unshift(last.value)  
    last.value = _createHistoryRecord()  
  
    if (options.capacity && undoStack.value.length > options.capacity)  
      undoStack.value.splice(options.capacity, Number.POSITIVE_INFINITY)  
    if (redoStack.value.length)  
      redoStack.value.splice(0, redoStack.value.length)  
  }  
  const clear = () => {  
    undoStack.value.splice(0, undoStack.value.length)  
    redoStack.value.splice(0, redoStack.value.length)  
  }  
  const undo = () => {  
    const state = undoStack.value.shift()  
  
    if (state) {  
      redoStack.value.unshift(last.value)  
      _setSource(state)  
    }  }  
  const redo = () => {  
    const state = redoStack.value.shift()  
  
    if (state) {  
      undoStack.value.unshift(last.value)  
      _setSource(state)  
    }  }  
  const reset = () => {  
    _setSource(last.value)  
  }  
  const history = computed(() => [last.value, ...undoStack.value])  
  
  const canUndo = computed(() => undoStack.value.length > 0)  
  const canRedo = computed(() => redoStack.value.length > 0)  
  
  return {  
    source,  
    undoStack,  
    redoStack,  
    last,  
    history,  
    canUndo,  
    canRedo,  
  
    clear,  
    commit,  
    reset,  
    undo,  
    redo,  
  }}
  • 从源码可以看到,内部似乎并没有做过多的操作,把所有的操作权都给了用户
  • 作用不一一细说了

10.useRefHistory

跟踪 ref 的变化历史,还提供了撤销和重做功能。

单元测试

import { nextTick, ref } from 'vue-demi'  
import { describe, expect, it } from 'vitest'  
import { useRefHistory } from '.'  
  
describe('useRefHistory - sync', () => {  
  it('sync: should record', () => {  
    const v = ref(0)  
    const { history } = useRefHistory(v, { flush: 'sync' })  
  
    expect(history.value.length).toBe(1)  
    expect(history.value[0].snapshot).toBe(0)  
  
    v.value = 2  
  
    expect(history.value.length).toBe(2)  
    expect(history.value[0].snapshot).toBe(2)  
    expect(history.value[1].snapshot).toBe(0)  
  })  
  it('sync: should be able to undo and redo', () => {  
    const v = ref(0)  
    const { undo, redo, clear, canUndo, canRedo, history, last } = useRefHistory(v, { flush: 'sync' })  
  
    expect(canUndo.value).toBe(false)  
    expect(canRedo.value).toBe(false)  
  
    v.value = 2  
    v.value = 3  
    v.value = 4  
  
    expect(canUndo.value).toBe(true)  
    expect(canRedo.value).toBe(false)  
  
    expect(v.value).toBe(4)  
    expect(history.value.length).toBe(4)  
    expect(last.value.snapshot).toBe(4)  
    undo()  
  
    expect(canUndo.value).toBe(true)  
    expect(canRedo.value).toBe(true)  
  
    expect(v.value).toBe(3)  
    expect(last.value.snapshot).toBe(3)  
    undo()  
    expect(v.value).toBe(2)  
    expect(last.value.snapshot).toBe(2)  
    redo()  
    expect(v.value).toBe(3)  
    expect(last.value.snapshot).toBe(3)  
    redo()  
    expect(v.value).toBe(4)  
    expect(last.value.snapshot).toBe(4)  
  
    expect(canUndo.value).toBe(true)  
    expect(canRedo.value).toBe(false)  
  
    redo()  
    expect(v.value).toBe(4)  
    expect(last.value.snapshot).toBe(4)  
  
    clear()  
    expect(canUndo.value).toBe(false)  
    expect(canRedo.value).toBe(false)  
  })
})
  • 所有操作权几乎提供给了用户,直接看实现吧

源码

import type { ConfigurableEventFilter, Fn } from '@vueuse/shared'  
import { pausableFilter, watchIgnorable } from '@vueuse/shared'  
import type { Ref } from 'vue-demi'  
import type { CloneFn } from '../useCloned'  
import type { UseManualRefHistoryReturn } from '../useManualRefHistory'  
import { useManualRefHistory } from '../useManualRefHistory'  
  
export interface UseRefHistoryOptions<Raw, Serialized = Raw> extends ConfigurableEventFilter {  
  /**  
   * Watch for deep changes, default to false   *   * When set to true, it will also create clones for values store in the history   *   * @default false  
   */  deep?: boolean  
  
  /**  
   * The flush option allows for greater control over the timing of a history point, default to 'pre'   *   * Possible values: 'pre', 'post', 'sync'   * It works in the same way as the flush option in watch and watch effect in vue reactivity   *   * @default 'pre'  
   */  flush?: 'pre' | 'post' | 'sync'  
  
  /**  
   * 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  
}  
  
export interface UseRefHistoryReturn<Raw, Serialized> extends UseManualRefHistoryReturn<Raw, Serialized> {  
  /**  
   * A ref representing if the tracking is enabled   */  isTracking: Ref<boolean>  
  
  /**  
   * Pause change tracking   */  pause: () => void  
  
  /**  
   * Resume change tracking   *   * @param [commit] if true, a history record will be create after resuming  
   */  resume: (commit?: boolean) => void  
  
  /**  
   * A sugar for auto pause and auto resuming within a function scope   *   * @param fn  
   */  
  batch: (fn: (cancel: Fn) => void) => void  
  
  /**  
   * Clear the data and stop the watch   */  dispose: () => void  
}  
  
/**  
 * Track the change history of a ref, also provides undo and redo functionality. * * @see https://vueuse.org/useRefHistory  
 * @param source  
 * @param options  
 */  
export function useRefHistory<Raw, Serialized = Raw>(  
  source: Ref<Raw>,  
  options: UseRefHistoryOptions<Raw, Serialized> = {},  
): UseRefHistoryReturn<Raw, Serialized> {  
  const {  
    deep = false,  
    flush = 'pre',  
    eventFilter,  
  } = options  
  
  const {  
    eventFilter: composedFilter,  
    pause,  
    resume: resumeTracking,  
    isActive: isTracking,  
  } = pausableFilter(eventFilter)  
  
  const {  
    ignoreUpdates,  
    ignorePrevAsyncUpdates,  
    stop,  
  } = watchIgnorable(  
    source,  
    commit,  
    { deep, flush, eventFilter: composedFilter },  
  )  
  function setSource(source: Ref<Raw>, value: Raw) {  
    // Support changes that are done after the last history operation  
    // examples:    //   undo, modify    //   undo, undo, modify    // If there were already changes in the state, they will be ignored    // examples:    //   modify, undo    //   undo, modify, undo    ignorePrevAsyncUpdates()  
  
    ignoreUpdates(() => {  
      source.value = value  
    })  
  }  
  const manualHistory = useManualRefHistory(source, { ...options, clone: options.clone || deep, setSource })  
  
  const { clear, commit: manualCommit } = manualHistory  
  
  function commit() {  
    // This guard only applies for flush 'pre' and 'post'  
    // If the user triggers a commit manually, then reset the watcher    // so we do not trigger an extra commit in the async watcher    ignorePrevAsyncUpdates()  
  
    manualCommit()  
  }  
  function resume(commitNow?: boolean) {  
    resumeTracking()  
    if (commitNow)  
      commit()  
  }  
  function batch(fn: (cancel: Fn) => void) {  
    let canceled = false  
  
    const cancel = () => canceled = true  
  
    ignoreUpdates(() => {  
      fn(cancel)  
    })  
    if (!canceled)  
      commit()  
  }  
  function dispose() {  
    stop()  
    clear()  
  }  return {  
    ...manualHistory,  
    isTracking,  
    pause,  
    resume,  
    commit,  
    batch,  
    dispose,  
  }}
  • 没啥好说的,看源码吧,更直观

11.useStorage

创建一个响应式引用,用于访问和修改 LocalStorage 或 SessionStorage

默认情况下使用 localStorage,其他存储源可以通过第三个参数指定。

单元测试(部分代码,剩下就是对不同数据类型存储处理)

import { debounceFilter, promiseTimeout } from '@vueuse/shared'  
import { defineComponent, isVue3, nextTick, ref, toRaw } from 'vue-demi'  
import { beforeEach, describe, expect, it, vi } from 'vitest'  
import { mount, nextTwoTick, useSetup } from '../../.test'  
import { StorageSerializers, customStorageEventName, useStorage } from '.'  
  
const KEY = 'custom-key'  
  
vi.mock('../ssr-handlers', () => ({  
  getSSRHandler: vi.fn().mockImplementationOnce((_, cb) => () => cb()).mockImplementationOnce(() => () => {  
    throw new Error('getDefaultStorage error')  
  }),}))  
  
describe('useStorage', () => {  
  console.error = vi.fn()  
  const storageState = new Map<string, string | number | undefined>()  
  const storageMock = {  
    getItem: vi.fn(x => storageState.get(x)),  
    setItem: vi.fn((x, v) => storageState.set(x, v)),  
    removeItem: vi.fn(x => storageState.delete(x)),  
    clear: vi.fn(() => storageState.clear()),  
  }  const storage = storageMock as any as Storage  
  
  beforeEach(() => {  
    localStorage.clear()  
    storageState.clear()  
    storageMock.setItem.mockClear()  
    storageMock.getItem.mockClear()  
    storageMock.removeItem.mockClear()  
  })  
  it('export module', () => {  
    expect(useStorage).toBeDefined()  
    expect(StorageSerializers).toBeDefined()  
  })  
  it('string', async () => {  
    const vm = useSetup(() => {  
      const ref = useStorage(KEY, 'a', storage)  
  
      return {  
        ref,  
      }    })  
    expect(vm.ref).toBe('a')  
    expect(storage.setItem).toBeCalledWith(KEY, 'a')  
  
    vm.ref = 'b'  
    await nextTwoTick()  
  
    expect(vm.ref).toBe('b')  
    expect(storage.setItem).toBeCalledWith(KEY, 'b')  
  })
})
  • 大概就是,key value和选择存储方式,核心点在于他实现响应式,大概就是监听修改,然后更新 storage,不知道是不是,看看源码吧 源码
import { nextTick, ref, shallowRef } from 'vue-demi'  
import type { Awaitable, ConfigurableEventFilter, ConfigurableFlush, MaybeRefOrGetter, RemovableRef } from '@vueuse/shared'  
import { pausableWatch, toValue, tryOnMounted } from '@vueuse/shared'  
import type { StorageLike } from '../ssr-handlers'  
import { getSSRHandler } from '../ssr-handlers'  
import { useEventListener } from '../useEventListener'  
import type { ConfigurableWindow } from '../_configurable'  
import { defaultWindow } from '../_configurable'  
import { guessSerializerType } from './guess'  
  
export interface Serializer<T> {  
  read: (raw: string) => T  
  write: (value: T) => string  
}  
  
export interface SerializerAsync<T> {  
  read: (raw: string) => Awaitable<T>  
  write: (value: T) => Awaitable<string>  
}  
  
export const StorageSerializers: Record<'boolean' | 'object' | 'number' | 'any' | 'string' | 'map' | 'set' | 'date', Serializer<any>> = {  
  boolean: {  
    read: (v: any) => v === 'true',  
    write: (v: any) => String(v),  
  },  object: {  
    read: (v: any) => JSON.parse(v),  
    write: (v: any) => JSON.stringify(v),  
  },  number: {  
    read: (v: any) => Number.parseFloat(v),  
    write: (v: any) => String(v),  
  },  any: {  
    read: (v: any) => v,  
    write: (v: any) => String(v),  
  },  string: {  
    read: (v: any) => v,  
    write: (v: any) => String(v),  
  },  map: {  
    read: (v: any) => new Map(JSON.parse(v)),  
    write: (v: any) => JSON.stringify(Array.from((v as Map<any, any>).entries())),  
  },  set: {  
    read: (v: any) => new Set(JSON.parse(v)),  
    write: (v: any) => JSON.stringify(Array.from(v as Set<any>)),  
  },  date: {  
    read: (v: any) => new Date(v),  
    write: (v: any) => v.toISOString(),  
  },}  
  
export const customStorageEventName = 'vueuse-storage'  
  
export interface StorageEventLike {  
  storageArea: StorageLike | null  
  key: StorageEvent['key']  
  oldValue: StorageEvent['oldValue']  
  newValue: StorageEvent['newValue']  
}  
  
export interface UseStorageOptions<T> extends ConfigurableEventFilter, ConfigurableWindow, ConfigurableFlush {  
  /**  
   * Watch for deep changes   *   * @default true  
   */  
   deep?: boolean  
  
  /**  
   * Listen to storage changes, useful for multiple tabs application   *   * @default true  
   */  
   listenToStorageChanges?: boolean  
  
  /**  
   * Write the default value to the storage when it does not exist   *   * @default true  
   */  
   writeDefaults?: boolean  
  
  /**  
   * Merge the default value with the value read from the storage.   *   * When setting it to true, it will perform a **shallow merge** for objects.   * You can pass a function to perform custom merge (e.g. deep merge), for example:   *   * @default false  
   */  
   mergeDefaults?: boolean | ((storageValue: T, defaults: T) => T)  
  
  /**  
   * Custom data serialization   */  
   serializer?: Serializer<T>  
  
  /**  
   * On error callback   *   * Default log error to `console.error`   */  
   onError?: (error: unknown) => void  
  
  /**  
   * Use shallow ref as reference   *   * @default false  
   */  
   shallow?: boolean  
  
  /**  
   * Wait for the component to be mounted before reading the storage.   *   * @default false  
   */  
   initOnMounted?: boolean  
}  
  
export function useStorage(key: string, defaults: MaybeRefOrGetter<string>, storage?: StorageLike, options?: UseStorageOptions<string>): RemovableRef<string>  
export function useStorage(key: string, defaults: MaybeRefOrGetter<boolean>, storage?: StorageLike, options?: UseStorageOptions<boolean>): RemovableRef<boolean>  
export function useStorage(key: string, defaults: MaybeRefOrGetter<number>, storage?: StorageLike, options?: UseStorageOptions<number>): RemovableRef<number>  
export function useStorage<T>(key: string, defaults: MaybeRefOrGetter<T>, storage?: StorageLike, options?: UseStorageOptions<T>): RemovableRef<T>  
export function useStorage<T = unknown>(key: string, defaults: MaybeRefOrGetter<null>, storage?: StorageLike, options?: UseStorageOptions<T>): RemovableRef<T>  
  
/**  
 * Reactive LocalStorage/SessionStorage. * * @see https://vueuse.org/useStorage  
 */  
export function useStorage<T extends(string | number | boolean | object | null)>(  
  key: string,  
  defaults: MaybeRefOrGetter<T>,  
  storage: StorageLike | undefined,  
  options: UseStorageOptions<T> = {},  
): RemovableRef<T> {  
  const {  
    flush = 'pre',  
    deep = true,  
    listenToStorageChanges = true,  
    writeDefaults = true,  
    mergeDefaults = false,  
    shallow,  
    window = defaultWindow,  
    eventFilter,  
    onError = (e) => {  
      console.error(e)  
    },    initOnMounted,  
  } = options  
  
  const data = (shallow ? shallowRef : ref)(typeof defaults === 'function' ? defaults() : defaults) as RemovableRef<T>  
  
  if (!storage) {  
    try {  
      storage = getSSRHandler('getDefaultStorage', () => defaultWindow?.localStorage)()  
    }    catch (e) {  
      onError(e)  
    }  }  
  if (!storage)  
    return data  
  
  const rawInit: T = toValue(defaults)  
  const type = guessSerializerType<T>(rawInit)  
  const serializer = options.serializer ?? StorageSerializers[type]  
  
  const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(  
    data,  
    () => write(data.value),  
    { flush, deep, eventFilter },  
  )  
  if (window && listenToStorageChanges) {  
    tryOnMounted(() => {  
      // this should be fine since we are in a mounted hook  
      useEventListener(window, 'storage', update)  
      useEventListener(window, customStorageEventName, updateFromCustomEvent)  
      if (initOnMounted)  
        update()  
    })  }  
  // avoid reading immediately to avoid hydration mismatch when doing SSR  
  if (!initOnMounted)  
    update()  
  
  function dispatchWriteEvent(oldValue: string | null, newValue: string | null) {  
    // send custom event to communicate within same page  
    // importantly this should _not_ be a StorageEvent since those cannot    // be constructed with a non-built-in storage area    if (window) {  
      window.dispatchEvent(new CustomEvent<StorageEventLike>(customStorageEventName, {  
        detail: {  
          key,  
          oldValue,  
          newValue,  
          storageArea: storage!,  
        },      }))    }  }  
  function write(v: unknown) {  
    try {  
      const oldValue = storage!.getItem(key)  
  
      if (v == null) {  
        dispatchWriteEvent(oldValue, null)  
        storage!.removeItem(key)  
      }      else {  
        const serialized = serializer.write(v as any)  
        if (oldValue !== serialized) {  
          storage!.setItem(key, serialized)  
          dispatchWriteEvent(oldValue, serialized)  
        }      }    }    catch (e) {  
      onError(e)  
    }  }  
  function read(event?: StorageEventLike) {  
    const rawValue = event  
      ? event.newValue  
      : storage!.getItem(key)  
  
    if (rawValue == null) {  
      if (writeDefaults && rawInit != null)  
        storage!.setItem(key, serializer.write(rawInit))  
      return rawInit  
    }  
    else if (!event && mergeDefaults) {  
      const value = serializer.read(rawValue)  
      if (typeof mergeDefaults === 'function')  
        return mergeDefaults(value, rawInit)  
      else if (type === 'object' && !Array.isArray(value))  
        return { ...rawInit as any, ...value }  
      return value  
    }  
    else if (typeof rawValue !== 'string') {  
      return rawValue  
    }  
    else {  
      return serializer.read(rawValue)  
    }  }  
  function update(event?: StorageEventLike) {  
    if (event && event.storageArea !== storage)  
      return  
  
    if (event && event.key == null) {  
      data.value = rawInit  
      return  
    }  
  
    if (event && event.key !== key)  
      return  
  
    pauseWatch()  
    try {  
      if (event?.newValue !== serializer.write(data.value))  
        data.value = read(event)  
    }    catch (e) {  
      onError(e)  
    }    finally {  
      // use nextTick to avoid infinite loop  
      if (event)  
        nextTick(resumeWatch)  
      else  
        resumeWatch()  
    }  }  
  function updateFromCustomEvent(event: CustomEvent<StorageEventLike>) {  
    update(event.detail)  
  }  
  return data  
}
  • 不出所料哈,大概就是包装了一层 ref,或者 shallowRef,当每次调用了 updated后都会执行一次 resumeWatch,啥效果不太知道
  • 在这里使用了 window.dispatchEvent 更新了 storage 里的数据 storage_event

12.useStorageAsync

具有异步支持的响应式 Storage。

没有单元测试,看源码

import type { MaybeRefOrGetter, RemovableRef } from '@vueuse/shared'  
import { toValue, watchWithFilter } from '@vueuse/shared'  
import type { Ref } from 'vue-demi'  
import { ref, shallowRef } from 'vue-demi'  
import type { StorageLikeAsync } from '../ssr-handlers'  
import { getSSRHandler } from '../ssr-handlers'  
import type { SerializerAsync, UseStorageOptions } from '../useStorage'  
import { StorageSerializers } from '../useStorage'  
import { useEventListener } from '../useEventListener'  
import { guessSerializerType } from '../useStorage/guess'  
import { defaultWindow } from '../_configurable'  
  
export interface UseStorageAsyncOptions<T> extends Omit<UseStorageOptions<T>, 'serializer'> {  
  /**  
   * Custom data serialization   */  serializer?: SerializerAsync<T>  
}  
  
export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter<string>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<string>): RemovableRef<string>  
export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter<boolean>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<boolean>): RemovableRef<boolean>  
export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter<number>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<number>): RemovableRef<number>  
export function useStorageAsync<T>(key: string, initialValue: MaybeRefOrGetter<T>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<T>): RemovableRef<T>  
export function useStorageAsync<T = unknown>(key: string, initialValue: MaybeRefOrGetter<null>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<T>): RemovableRef<T>  
  
/**  
 * Reactive Storage in with async support. * * @see https://vueuse.org/useStorageAsync  
 * @param key  
 * @param initialValue  
 * @param storage  
 * @param options  
 */  
export function useStorageAsync<T extends(string | number | boolean | object | null)>(  
  key: string,  
  initialValue: MaybeRefOrGetter<T>,  
  storage: StorageLikeAsync | undefined,  
  options: UseStorageAsyncOptions<T> = {},  
): RemovableRef<T> {  
  const {  
    flush = 'pre',  
    deep = true,  
    listenToStorageChanges = true,  
    writeDefaults = true,  
    mergeDefaults = false,  
    shallow,  
    window = defaultWindow,  
    eventFilter,  
    onError = (e) => {  
      console.error(e)  
    },  } = options  
  
  const rawInit: T = toValue(initialValue)  
  const type = guessSerializerType<T>(rawInit)  
  
  const data = (shallow ? shallowRef : ref)(initialValue) as Ref<T>  
  const serializer = options.serializer ?? StorageSerializers[type]  
  
  if (!storage) {  
    try {  
      storage = getSSRHandler('getDefaultStorageAsync', () => defaultWindow?.localStorage)()  
    }    catch (e) {  
      onError(e)  
    }  }  
  async function read(event?: StorageEvent) {  
    if (!storage || (event && event.key !== key))  
      return  
  
    try {  
      const rawValue = event ? event.newValue : await storage.getItem(key)  
      if (rawValue == null) {  
        data.value = rawInit  
        if (writeDefaults && rawInit !== null)  
          await storage.setItem(key, await serializer.write(rawInit))  
      }      else if (mergeDefaults) {  
        const value = await serializer.read(rawValue)  
        if (typeof mergeDefaults === 'function')  
          data.value = mergeDefaults(value, rawInit)  
        else if (type === 'object' && !Array.isArray(value))  
          data.value = { ...(rawInit as any), ...value }  
        else data.value = value  
      }  
      else {  
        data.value = await serializer.read(rawValue)  
      }    }    catch (e) {  
      onError(e)  
    }  }  
  read()  
  
  if (window && listenToStorageChanges)  
    useEventListener(window, 'storage', e => Promise.resolve().then(() => read(e)))  
  
  if (storage) {  
    watchWithFilter(  
      data,  
      async () => {  
        try {  
          if (data.value == null)  
            await storage!.removeItem(key)  
          else  
            await storage!.setItem(key, await serializer.write(data.value))  
        } catch (e) {  
          onError(e)  
        }      
      },      
      { 
        flush,  
        deep,  
        eventFilter,  
      },    
    )  
  }  
  return data as RemovableRef<T>  
}
  • 使用了异步来更新 storage

13.useThrottledRefHistory

带有节流过滤器的 useRefHistory 的简写。

单元测试

import { ref } from 'vue-demi'  
import { promiseTimeout } from '@vueuse/shared'  
import { describe, expect, it } from 'vitest'  
import { useThrottledRefHistory } from '.'  
  
describe('useThrottledRefHistory - sync', () => {  
  it('take first snapshot right after data was changed and second after given time', async () => {  
    const ms = 10  
    const v = ref(0)  
  
    const { history } = useThrottledRefHistory(v, { throttle: ms })  
  
    expect(history.value.length).toBe(1)  
    expect(history.value[0].snapshot).toBe(0)  
  
    v.value = 100  
  
    await promiseTimeout(ms * 3)  
  
    expect(history.value.length).toBe(2)  
    expect(history.value[0].snapshot).toBe(100)  
  
    v.value = 200  
    v.value = 300  
    v.value = 400  
  
    await promiseTimeout(ms * 3)  
  
    expect(history.value.length).toBe(3)  
    expect(history.value[0].snapshot).toBe(400)  
  })})
  • 就是给 useRefHistory 加上了一个节流功能,和上方 useDebounceRefHistory 类似 源码
import type { MaybeRef } from '@vueuse/shared'  
import { throttleFilter } from '@vueuse/shared'  
import type { Ref } from 'vue-demi'  
import type { UseRefHistoryOptions, UseRefHistoryReturn } from '../useRefHistory'  
import { useRefHistory } from '../useRefHistory'  
  
export type UseThrottledRefHistoryOptions<Raw, Serialized = Raw> = Omit<UseRefHistoryOptions<Raw, Serialized>, 'eventFilter'> & { throttle?: MaybeRef<number>, trailing?: boolean }  
  
export type UseThrottledRefHistoryReturn<Raw, Serialized = Raw> = UseRefHistoryReturn<Raw, Serialized>  
  
/**  
 * Shorthand for [useRefHistory](https://vueuse.org/useRefHistory) with throttled filter.  
 * * @see https://vueuse.org/useThrottledRefHistory  
 * @param source  
 * @param options  
 */  
export function useThrottledRefHistory<Raw, Serialized = Raw>(  
  source: Ref<Raw>,  
  options: UseThrottledRefHistoryOptions<Raw, Serialized> = {},  
): UseThrottledRefHistoryReturn<Raw, Serialized> {  
  const { throttle = 200, trailing = true } = options  
  const filter = throttleFilter(throttle, trailing)  
  const history = useRefHistory(source, { ...options, eventFilter: filter })  
  
  return {  
    ...history,  
  }}
转载自:https://juejin.cn/post/7379487557598838821
评论
请登录