VueUse探索1: useCounter 是如何实现的?

124 阅读4分钟

前言

VueUse 是什么?根据官网的说法,VueUse 是依据 Vue 组合 API 的工具函数的集合。

那么为什么要使用 VueUse? 当然是为了使得代码编写更加高效和快捷。

接下来我们以 useCounter(计数器) 为例子,探索下 VueUse 的用法和实现原理。

基础用法

官方文档可以看到,基础用法如下:

<script setup>
import { useCounter } from '@vueuse/core'
const { count, inc, dec, set, reset } = useCounter(1, { min: 0, max: 16 })
</script>

<div>
  {{count}}
  <div @click="inc()">+</div>
  <div @click="dec()">-</div>
  <div @click="inc(3)">+3</div>
  <div @click="reset()">reset</div>
</div>

引入@vueuse/core后,我们就可以通过useCounter()的方式引入变量 count 以及 inc、dec、reset 等函数。

很简单的用法,点击 + 时数字+1,点击 - 时数字 -1,也可以传参想要加减的幅度。点击 reset 时会恢复到传参 1。

可以去 playground 在线测试下。

实现原理

完整源码

// packages\shared\useCounter\index.ts

import type { MaybeRef, Ref } from 'vue'
import {
  shallowReadonly,
  shallowRef,
  // eslint-disable-next-line no-restricted-imports
  unref,
} from 'vue'

export interface UseCounterOptions {
  min?: number
  max?: number
}

export interface UseCounterReturn {
  /**
   * The current value of the counter.
   */
  readonly count: Readonly<Ref<number>>
  /**
   * Increment the counter.
   *
   * @param {number} [delta=1] The number to increment.
   */
  inc: (delta?: number) => void
  /**
   * Decrement the counter.
   *
   * @param {number} [delta=1] The number to decrement.
   */
  dec: (delta?: number) => void
  /**
   * Get the current value of the counter.
   */
  get: () => number
  /**
   * Set the counter to a new value.
   *
   * @param val The new value of the counter.
   */
  set: (val: number) => void
  /**
   * Reset the counter to an initial value.
   */
  reset: (val?: number) => number
}

/**
 * Basic counter with utility functions.
 *
 * @see https://vueuse.org/useCounter
 * @param [initialValue]
 * @param options
 */
export function useCounter(initialValue: MaybeRef<number> = 0, options: UseCounterOptions = {}) {
  let _initialValue = unref(initialValue)
  const count = shallowRef(initialValue)

  const {
    max = Number.POSITIVE_INFINITY,
    min = Number.NEGATIVE_INFINITY,
  } = options

  const inc = (delta = 1) => count.value = Math.max(Math.min(max, count.value + delta), min)
  const dec = (delta = 1) => count.value = Math.min(Math.max(min, count.value - delta), max)
  const get = () => count.value
  const set = (val: number) => (count.value = Math.max(min, Math.min(max, val)))
  const reset = (val = _initialValue) => {
    _initialValue = val
    return set(val)
  }

  return { count: shallowReadonly(count), inc, dec, get, set, reset }
}

首先,我们可以看到,useCounter函数的参数是 initialValue 和 options,在例子中我们将初始值设置为 1,配置项 options 设置了{ min: 0, max: 16 }

在函数内部,初始值会记录在变量 _initialValue 以及 count 中,

当每次触发 inc 函数时,默认增幅 delta 是 1(可以重新传参),count的值会更新为Math.max(Math.min(max, count.value + delta), min)。也就是说,在原 count 的基础上加上增幅 delta,并控制不超过 max,实现方法是Math.min(max, count.value + delta)。还要控制不限于 min,实现方法是Math.max(xxx, min)

类似地,dec 函数是差不多的实现方法。

get 和 set 函数是获取数值以及设置新的数值。

reset 函数,可以重置为传入的值或者之前保存的初始值 _initialValue,调用 set 方法去设置。

其次,我们不难发现,代码中作者从 vue 中引入了 MaybeRef, shallowReadonly, shallowRef, unref 等,这些是什么意思呢?

MaybeRef, unref

在 VueUse 中,MaybeRef 是一个类型工具,表示一个值可以是普通值或 Vue 的响应式引用(Ref)。这种设计使得 API 更加灵活,既支持直接传入静态值,也支持响应式变量。

export type MaybeRef<T = any> = T | Ref<T>;

在代码中,初始值 initialValue 是 MaybeRef 类型的,说明我们可以传参:

  • 基础变量(如 42)
  • Vue 的 Ref 对象(如 ref(42))

而内部变量 _initialValue ,会经过 unref() 处理:

let _initialValue = unref(initialValue)

换句话说, unref 会将 ref 变量或者基础变量转为基础变量返回:

export declare function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T;

使用场景:

const a = ref(1)
const b = 2
console.log(unref(a)) // 1
console.log(unref(b)) // 2

shallowReadonly

shallowReadonly 用于创建一个浅层只读的响应式对象。特点是:

  • 只读性:返回的对象是只读的,不能直接修改其属性
  • 浅层响应:只会对对象的第一层属性做响应式处理,嵌套对象保持原样

因此,shallowReadonly 性能比 readonly 更好,适合不需要深度响应式的场景。

源码是:

export declare function shallowReadonly<T extends object>(target: T): Readonly<T>;

在 useCounter 中,返回 count 时用到了:

return { count: shallowReadonly(count), inc, dec, get, set, reset }

这样做的目的是:

防止外部直接修改 count.value(强制通过提供的 inc/dec/set 方法修改)

保持性能优化,因为计数器值本身就是基本类型数字,不需要深度响应式

shallowRef

shallowRef 用于创建一个浅层响应式的引用(Ref)。它与常规 ref 的主要区别是:

  • 浅层响应:只对 .value 本身做响应式处理,不会递归转换嵌套对象
  • 性能优化:比 ref 更轻量,适合性能敏感、不需要深度响应式的场景

在 useCounter 中的使用:

const count = shallowRef(initialValue) // 创建一个浅层响应式计数器

当 count.value 被修改时,会触发更新;如果传入的 initialValue 是对象,那么修改对象内部属性时不会触发更新(除非替换整个对象)。

对比 ref:

const deepRef = ref({ a: 1 })
const shallow = shallowRef({ a: 1 })
deepRef.value.a = 2  // 会触发依赖更新
shallow.value.a = 2  // 不会触发更新

后记

麻雀虽小,五脏俱全。本文以最简单的 useCounter(计数器) 为例子,探索 VueUse 的用法和实现原理。

使用 VueUse,可以让我们编写代码更加地高效和快捷。