React Hook 与 Composition API 概念是很相似的。事实上在 React 大部分可用的 Hook 都可以使用 Vue3 再实现一遍,但是 Composition API 语法需要加强去了解一下。
以下示例代码均不复杂,还需结合实际业务场景开发。示例中有参考
ahooks、VueUse 源码。(当然!也可以直接使用 ahooks VueUse)
useDebounce
用来处理防抖函数的 Hook。
Params
| 参数 | 说明 | 类型 |
|---|---|---|
| callback | 需要防抖的函数 | (...args: any[]) => any |
| delay | 等待时间,单位为毫秒 | number |
Result
| 参数 | 说明 | 类型 |
|---|---|---|
| run | 触发执行 callback,函数参数将会传递给 callback | (...args: any[]) => any |
| cancel | 取消当前防抖 | () => void |
import { ref, onUnmounted } from 'vue'
/**
* @param {Function} callback - 需要防抖的函数
* @param {number} delay - 防抖时间,默认100ms
*/
export default function useDebounce<T extends (...args: any[]) => any>(callback: T, delay = 100) {
const timer = ref<NodeJS.Timeout | ReturnType<typeof setTimeout> | null>(null)
const run = (...args: Parameters<T>) => {
cancel()
timer.value = setTimeout(() => callback(...args), delay)
}
const cancel = () => {
timer.value !== null && clearTimeout(timer.value)
timer.value = null
}
onUnmounted(cancel)
return {
run,
cancel
}
}
useThrottle
用来处理函数节流的 Hook。
Params
| 参数 | 说明 | 类型 |
|---|---|---|
| callback | 需要节流的函数 | (...args: any[]) => any |
| delay | 等待时间,单位为毫秒 | number |
Result
| 参数 | 说明 | 类型 |
|---|---|---|
| run | 触发执行 callback,函数参数将会传递给 callback | (...args: any[]) => any |
| cancel | 取消当前节流 | () => void |
import { onUnmounted, ref } from 'vue'
/**
* @param {Function} callback - 要执行的回调函数
* @param {number} delay - 延迟时间,单位为毫秒,默认为100毫秒
*/
export default function useThrottle<T extends (...args: any[]) => void>(callback: T, delay = 100) {
const timer = ref<NodeJS.Timeout | ReturnType<typeof setTimeout> | null>(null)
const run = (...args: Parameters<T>) => {
if (timer.value) return
timer.value = setTimeout(() => {
callback(...args)
cancel()
}, delay)
}
const cancel = () => {
timer.value && clearTimeout(timer.value)
timer.value = null
}
onUnmounted(cancel)
return {
run,
cancel
}
}
useDebounce和useThrottle同样也可以使用Vue3 customRef去改造。
useEventBus
在应用程序中创建事件总线。在组件之间共享状态或信息时,提高代码的可维护性和可读性,同时减少组件之间的耦合性。
Result
| 参数 | 说明 | 类型 |
|---|---|---|
| emit | 发送一个事件通知 | (val: T) => void |
| useSubscription | 订阅事件 | (callback: (val: T) => void) => void |
import { ref, onUnmounted, onMounted } from 'vue'
import { Subscription } from './type'
/**
* 事件订阅器,用于管理事件的订阅和发布
*/
export class EventEmitter<T> {
private readonly subscriptions = new Set<Subscription<T>>()
/**
* 发布事件
* @param val 事件数据
*/
emit = (val: T) => {
for (const subscription of this.subscriptions) {
subscription(val)
}
}
/**
* 订阅事件
* @param callback 事件回调函数
*/
useSubscription = (callback: Subscription<T>) => {
const callbackRef = ref<Subscription<T>>()
callbackRef.value = callback
onMounted(() => {
const subscription = (val: T) => callbackRef.value?.(val)
this.subscriptions.add(subscription)
})
onUnmounted(() => this.subscriptions.clear())
}
}
/**
* 事件订阅器的自定义 Hook
* @returns 事件订阅器实例
*/
export default function useEventBus<T = void>() {
const eventEmitterRef = ref<EventEmitter<T>>()
if (eventEmitterRef.value == null) eventEmitterRef.value = new EventEmitter()
return eventEmitterRef.value
}
useImage
在浏览器中响应式加载图像,你可以等待结果显示或显示一个回退方案
Params
| 参数 | 说明 | 类型 |
|---|---|---|
| options | 图片加载参数 | UseImageOptions |
| asyncStateOptions | 异步状态参数 | UseAsyncStateOptions |
UseImageOptions
| 参数 | 说明 | 类型 |
|---|---|---|
| src | 图片路径 | string |
| srcset | 在不同情况下使用的图像,例如高分辨率显示器、小型显示器等 | string |
| sizes | 不同页面布局的图像大小 | string |
UseAsyncStateOptions
| 参数 | 说明 | 类型 |
|---|---|---|
| delay | 超时时间 | number |
| immediate | 是否立即执行 | boolean |
| onError | 失败回调 | (e: unknown) => void |
| onSuccess | 成功回调 | (data: D) => void |
| resetOnExecute | 是否重置状态 | boolean |
Result
| 参数 | 说明 | 类型 |
|---|---|---|
| isLoading | 加载状态 | boolean |
| isReady | 成功状态 | boolean |
| error | 失败状态 | ShallowRef<unknown> |
import { watch, ref, shallowRef } from 'vue'
import { toValue } from '@/utils'
import { UseImageOptions, UseAsyncStateOptions } from './type'
import { MaybeRefOrGetter } from '@/utils/types'
function loadImage({ src, srcset, sizes }: UseImageOptions): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image()
img.src = src
if (srcset) img.srcset = srcset
if (sizes) img.sizes = sizes
img.onload = () => resolve(img)
img.onerror = reject
})
}
/**
* @param options 图片加载参数
* @param asyncStateOptions 异步状态参数
* @returns { isLoading, isReady, error }
*/
function useImage(
options: MaybeRefOrGetter<UseImageOptions>,
asyncStateOptions: UseAsyncStateOptions = {}
) {
const {
delay = 5000,
immediate = true,
onSuccess,
onError,
resetOnExecute = true
} = asyncStateOptions
const isLoading = ref(false)
const isReady = ref(false)
const error = shallowRef<unknown | undefined>(undefined)
const loadImageWithTimeout = async () => {
if (resetOnExecute) {
isLoading.value = true
isReady.value = false
}
return await Promise.race([
loadImage(toValue(options)),
new Promise((_resolve, reject) =>
setTimeout(() => reject(new Error(`Image loading timed out`)), delay)
)
])
.then((res) => {
isReady.value = true
onSuccess?.(res)
})
.catch((e) => {
error.value = e
onError?.(e)
})
.finally(() => (isLoading.value = false))
}
if (immediate) loadImageWithTimeout()
watch(
() => toValue(options),
() => loadImageWithTimeout(),
{ deep: true }
)
return {
isLoading,
isReady,
error
}
}
export default useImage
useInViewport
观察元素是否在可见区域,以及元素可见比例。 更多信息参考 Intersection Observer API。
Params
| 参数 | 说明 | 类型 |
|---|---|---|
| target | 目标元素 | Ref<T>|T |
| callback | 回调函数 | IntersectionObserverCallback |
| options | 设置项 | Options |
Options
| 参数 | 说明 | 类型 |
|---|---|---|
| root | 指定根(root)元素,用于检查目标的可见性。必须是目标元素的父级元素,如果未指定或者为 null,则默认为浏览器视窗。 | Ref<T>|T |
| rootMargin | 根(root)元素的外边距 | string |
| threshold | 可以是单一的 number 也可以是 number 数组,target 元素和 root 元素相交程度达到该值的时候 ratio 会被更新 | number|number[] |
Result
| 参数 | 说明 | 类型 |
|---|---|---|
| stop | 停止监听函数 | () => void |
import { ref, watch, onUnmounted } from 'vue'
import useSupported from '@/hooks/useSupported'
import { unrefElement } from '@/utils'
import { MaybeComputedElementRef } from '@/utils/types'
import { UseIntersectionObserverOptions } from './type'
/**
* @param target 目标元素
* @param callback 回调函数
* @param options 选项
* @returns 停止监听函数
*/
export default function useInViewport(
target: MaybeComputedElementRef,
callback: IntersectionObserverCallback,
options: UseIntersectionObserverOptions = {}
) {
const { root, rootMargin = '0px', threshold = 0.1 } = options
const isSupported = useSupported(() => window && 'IntersectionObserver' in window)
const intersectionObserverRef = ref<IntersectionObserver | null>(null)
const stop = () => intersectionObserverRef.value && intersectionObserverRef.value.disconnect()
watch(
() => [unrefElement(target), unrefElement(root)] as const,
([target, root]) => {
if (!isSupported.value || !target || !root) return
intersectionObserverRef.value = new IntersectionObserver(callback, {
rootMargin,
threshold,
root: unrefElement(root)
})
intersectionObserverRef.value.observe(target)
},
{
immediate: true,
flush: 'post'
}
)
onUnmounted(stop)
return { stop }
}
useIsWindowVisible
监听页面是否可见,参考 visibilityState API
Result
| 参数 | 说明 | 类型 |
|---|---|---|
| isVisible | 判断 document 是否在是否处于可见状态 | boolean |
import { ref, watchEffect, onUnmounted } from 'vue'
function isWindowVisible() {
if (!(typeof document !== 'undefined' && 'visibilityState' in document)) return true
return document.visibilityState === 'visible'
}
/**
* 判断窗口是否可见
* @param callback 回调函数,可选参数,当窗口可见性状态改变时调用
*/
function useIsWindowVisible(callback?: (event: boolean) => void) {
const isVisible = ref(isWindowVisible())
const handleVisibilityChange = () => (isVisible.value = isWindowVisible())
const clear = () => document.removeEventListener('visibilitychange', handleVisibilityChange)
watchEffect((onInvalidate) => {
if (!('visibilityState' in document)) return undefined
document.addEventListener('visibilitychange', handleVisibilityChange)
callback?.(isVisible.value)
onInvalidate(clear)
})
onUnmounted(clear)
return isVisible
}
export default useIsWindowVisible
useMutationObserver
一个监听指定的 DOM 树发生变化的 Hook,参考 MutationObserver
Params
| 参数 | 说明 | 类型 |
|---|---|---|
| target | 监听的元素 | Ref<T>|T |
| callback | 回调函数 | MutationCallback |
| options | 设置项 | MutationObserverInit |
Result
| 参数 | 说明 | 类型 |
|---|---|---|
| stop | 停止监听函数 | () => void |
import { ref, watch, onUnmounted } from 'vue'
import useSupported from '../useSupported'
import { unrefElement } from '@/utils'
import { UseMutationObserverOptions } from './type'
import { MaybeElementRef } from '@/utils/types'
/**
* @param {MaybeElementRef} target - 监听的元素
* @param {MutationCallback} callback - 回调函数
* @param {UseMutationObserverOptions} options - 配置项
* @returns {Object} - 返回一个对象,包含 stop 方法
*/
const useMutationObserver = (
target: MaybeElementRef,
callback: MutationCallback,
options: UseMutationObserverOptions = {}
) => {
const isSupported = useSupported(() => window && 'MutationObserver' in window)
const mutationObserverRef = ref<MutationObserver | null>(null)
const stop = () => mutationObserverRef.value && mutationObserverRef.value.disconnect()
watch(
() => unrefElement(target),
(el) => {
if (!isSupported.value || !el) return
mutationObserverRef.value = new MutationObserver(callback)
mutationObserverRef.value.observe(el, options)
},
{
immediate: true,
flush: 'post'
}
)
onUnmounted(stop)
return { stop }
}
export default useMutationObserver
useRequest
异步数据管理的 Hooks,案例展示仅实现:自动请求/手动请求。更多参考 ahooks - useRequest
Params
| 参数 | 说明 | 类型 |
|---|---|---|
| service | 请求函数类型 | (...args: P) => Promise<T> |
| options | 设置项 | Options |
Options
| 参数 | 说明 | 类型 |
|---|---|---|
| manual | 默认 false。 即在初始化时自动执行 service。如果设置为 true,则需要手动调用 run 触发执行。 | boolean |
| onSuccess | 成功回调 | (data: T, params: P) => void |
| onError | 失败回调 | (e: Error, params: P) => void |
Result
| 参数 | 说明 | 类型 |
|---|---|---|
| loading | service 是否正在执行 | boolean |
| data | service 返回的数据 | Ref<T> |
| error | service 抛出的异常 | Ref<Error> |
| run | 手动触发 service 执行,参数会传递给 service。异常自动处理,通过 onError 反馈 | (...args: P) => Promise<void> |
import { ref, onMounted } from 'vue'
import { noop } from '@/utils'
import { Service, Options } from './type'
/**
* @template T 请求成功后返回的数据类型
* @template P 请求函数的参数类型
* @param {(...args: P) => Promise<T>} Service 请求函数类型
* @param {object} Options 请求配置项
* @property {boolean} [manual=false] 是否手动触发请求
* @property {(data: T, args: P) => void} [onSuccess] 请求成功后的回调函数
* @property {(error: Error, args: P) => void} [onError] 请求失败后的回调函数
* @returns {{loading: Ref<boolean>, data: Ref<T | undefined>, error: Ref<Error | undefined>, run: (...args: P) => Promise<void>}}
*/
function useRequest<T, P extends any[]>(service: Service<T, P>, options?: Options<T, P>) {
const { manual = false, onSuccess = noop, onError = noop } = options ?? {}
const loading = ref(false)
const data = ref<T>()
const error = ref<Error>()
const run = async (...args: P) => {
loading.value = true
try {
const result = await service(...args)
data.value = result
onSuccess?.(result, args)
} catch (e) {
error.value = e as Error
onError?.(e as Error, args)
} finally {
loading.value = false
}
}
// @ts-expect-error
onMounted(() => !manual && run())
return {
loading,
data,
error,
run
}
}
export default useRequest
useSetInterval
一个可以处理 setInterval 的 Hook。
Params
| 参数 | 说明 | 类型 |
|---|---|---|
| callback | 定时器回调函数 | () => void |
| delay | 定时器间隔时间 | number |
import { ref, watchEffect, onUnmounted } from 'vue'
/**
* @param callback 定时器回调函数
* @param delay 定时器间隔时间,默认为1000ms
*/
function useSetInterval(callback: () => void, delay: number = 1000) {
const timer = ref<NodeJS.Timeout | ReturnType<typeof setTimeout> | null>(null)
const clear = () => timer.value && clearInterval(timer.value)
watchEffect((onInvalidate) => {
timer.value = setInterval(callback, delay)
onInvalidate(clear)
})
onUnmounted(clear)
}
export default useSetInterval
useTheme
在应用程序中轻松地使用主题变量,例如颜色、字体大小和样式,以及切换不同的主题,而无需手动管理这些值。可以节省大量的代码和时间。
Result
| 参数 | 说明 | 类型 |
|---|---|---|
| theme | 当前主题 | Ref<Theme> |
| toggle | 切换主题 | (val: Theme) => Theme |
import { ref, watch } from 'vue'
import { defineStore } from 'pinia'
import { Theme } from './type'
const useTheme = defineStore(
'theme',
() => {
const theme = ref('light')
const toggle = (val: Theme) => (theme.value = val)
watch(theme, (newVal, oldVal) => {
document.querySelector('html')?.classList.remove(oldVal)
document.querySelector('html')?.classList.add(newVal)
})
return { theme, toggle }
},
{
persist: true
}
)
export default useTheme