VueUse 常见能力详解
1. VueUse 是什么
@vueuse/core 是基于 Vue Composition API 构建的实用工具库。
它把常见的浏览器 API、DOM 操作、事件监听、定时器、本地存储、网络通信等能力,封装成符合 Vue 响应式风格的组合式函数(Composable)。
可以把它理解为:
Vue Composition API 的常用工具箱。
例如,原生方式监听窗口尺寸变化:
ts
import { onMounted, onUnmounted } from 'vue'
function handleResize() {
console.log(window.innerWidth)
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
使用 VueUse:
ts
import { useEventListener } from '@vueuse/core'
useEventListener(window, 'resize', () => {
console.log(window.innerWidth)
})
VueUse 通常能帮我们处理:
- 将结果转换为 Vue 响应式数据。
- 在组件挂载时注册监听。
- 在组件卸载时自动清理监听器、观察器和定时器。
- 提供
stop、pause、resume、execute等控制方法。 - 统一浏览器 API 与 Vue Composition API 的使用方式。
2. VueUse 是否只支持 Vue 3
需要根据 VueUse 版本判断:
- VueUse 9.13.0 同时支持 Vue 2 和 Vue 3。
- VueUse 11.x 是最后一批支持 Vue 2 的版本。
- VueUse 12 及之后只支持 Vue 3。
当前项目实际使用的是 VueUse 9.13.0,因此下面的 API 和示例主要以 9.13.0 为准。
官方文档:
3. VueUse 的基本使用方式
VueUse 一般采用按需导入:
ts
import {
onClickOutside,
useElementSize,
useEventListener,
useLocalStorage,
useWindowSize,
} from '@vueuse/core'
大多数 VueUse 函数会返回以下内容之一:
- 响应式
ref computed- 状态对象
- 启动、暂停、停止等控制函数
- 浏览器原始对象
例如:
ts
const { width, height } = useWindowSize()
这里的 width 和 height 是:
ts
Ref<number>
在 JavaScript 中需要访问 .value:
ts
console.log(width.value)
在 Vue 模板中会自动解包:
vue
<template>
<div>
当前窗口:{{ width }} × {{ height }}
</div>
</template>
第一梯队:非常常见
4. useEventListener:通用事件监听
4.1 解决什么问题
useEventListener 是响应式版本的:
ts
target.addEventListener()
target.removeEventListener()
它负责:
- 在合适的生命周期注册事件。
- 组件卸载时自动移除事件。
- 当目标
ref发生变化时,解绑旧元素并绑定新元素。 - 提供手动停止监听的方法。
4.2 监听 window
ts
import { useEventListener } from '@vueuse/core'
useEventListener(window, 'resize', () => {
console.log('窗口尺寸发生变化')
})
4.3 监听 document
ts
useEventListener(document, 'click', (event) => {
console.log('页面被点击', event.target)
})
4.4 监听 DOM 元素
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useEventListener } from '@vueuse/core'
const buttonRef = ref<HTMLElement | null>(null)
useEventListener(buttonRef, 'click', () => {
console.log('按钮被点击')
})
</script>
<template>
<button ref="buttonRef">
点击
</button>
</template>
4.5 手动停止监听
ts
const stop = useEventListener(window, 'resize', handleResize)
// 不再需要时手动停止
stop()
正常情况下组件卸载时会自动停止,不一定需要手动调用。
手动停止适合:
- 某个业务阶段结束后立即取消监听。
- 弹窗关闭后不再监听。
- 地图工具退出后释放事件。
- 监听器只需要执行一段时间。
4.6 常见应用场景
- 监听
window.resize - 监听
document.click - 监听鼠标移动
- 监听滚轮
- 监听键盘
- 监听 Cesium 容器
- 监听第三方库创建的 DOM
- 监听视频、音频事件
4.7 与原生事件相比的优势
原生写法:
ts
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
VueUse 写法:
ts
useEventListener(window, 'resize', handleResize)
VueUse 减少了生命周期管理代码,也降低了忘记清理事件造成内存泄漏的风险。
官方文档:
5. useLocalStorage:响应式永久本地存储
5.1 解决什么问题
useLocalStorage 把 Vue 的响应式状态与浏览器 localStorage 双向绑定。
ts
import { useLocalStorage } from '@vueuse/core'
const theme = useLocalStorage('app-theme', 'light')
修改响应式变量:
ts
theme.value = 'dark'
浏览器中会自动保存:
text
app-theme = dark
刷新页面后,状态仍然保留。
5.2 保存对象
ts
const mapSettings = useLocalStorage('map-settings', {
showLabels: true,
opacity: 1,
baseMap: 'satellite',
})
修改对象:
ts
mapSettings.value.opacity = 0.5
mapSettings.value.showLabels = false
数据会自动序列化后写入 localStorage。
5.3 保存数组
ts
const recentProjects = useLocalStorage<string[]>(
'recent-projects',
[],
)
recentProjects.value.push('project-001')
5.4 常见应用场景
- 用户主题设置
- 表格列配置
- 用户筛选条件
- 地图底图设置
- 地图图层显示偏好
- 最近访问项目
- 是否已经看过提示
- 页面布局偏好
- 非敏感草稿
5.5 注意事项
不要在 localStorage 中保存:
- 用户密码
- 高敏感业务数据
- 不能暴露的长期凭证
- 特别大的数据
- 大型 GeoJSON
- 文件二进制内容
还要注意:
Date序列化后通常变成字符串。Map、Set、类实例可能丢失类型。- 同一个 key 不要混用不同数据类型。
localStorage是同步 API,大数据读写可能阻塞主线程。- 它不能代替后端数据库。
- 它也不能代替完整的 Pinia 业务状态管理。
官方文档:
6. useSessionStorage:当前标签页会话存储
6.1 基本使用
ts
import { useSessionStorage } from '@vueuse/core'
const currentStep = useSessionStorage('import-step', 1)
修改状态:
ts
currentStep.value = 2
刷新当前页面后仍然可以恢复。
关闭当前标签页后,这份数据通常会被清除。
6.2 与 useLocalStorage 的区别
| 能力 | useLocalStorage |
useSessionStorage |
|---|---|---|
| 刷新页面后 | 保留 | 保留 |
| 关闭标签页后 | 保留 | 通常清除 |
| 新标签页 | 可以读取相同数据 | 通常属于独立会话 |
| 适合长期设置 | 是 | 否 |
| 适合临时流程 | 一般 | 是 |
6.3 常见应用场景
- 当前标签页的操作步骤
- 临时筛选状态
- 导入流程进度
- 当前会话内的草稿
- 临时展开状态
- 当前页面导航状态
官方文档:
7. onClickOutside:点击元素外部
7.1 解决什么问题
onClickOutside 用于实现:
点击某个元素之外的区域时执行回调。
常见于:
- 下拉菜单
- 地图图例
- 底图选择器
- 浮动工具栏
- Popover
- 自定义弹出面板
7.2 基本使用
vue
<script setup lang="ts">
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'
const panelRef = ref<HTMLElement | null>(null)
const visible = ref(false)
onClickOutside(panelRef, () => {
visible.value = false
})
</script>
<template>
<div v-if="visible" ref="panelRef">
面板内容
</div>
</template>
判断逻辑是:
text
点击 panelRef 内部
→ 不执行回调
点击 panelRef 外部
→ 执行回调
7.3 同时排除按钮和面板
实际开发中通常有两个元素:
- 打开面板的按钮
- 弹出的面板
ts
const triggerRef = ref<HTMLElement | null>(null)
const popupRef = ref<HTMLElement | null>(null)
const popupVisible = ref(false)
onClickOutside(
triggerRef,
() => {
if (popupVisible.value)
popupVisible.value = false
},
{
ignore: [popupRef],
},
)
此时行为是:
text
点击按钮
→ 不关闭
点击面板内部
→ 不关闭
点击其他区域
→ 关闭
7.4 ignore 的作用
ignore 表示哪些元素虽然不在目标元素内部,但仍然不应该被视为外部区域。
可以传 DOM ref:
ts
{
ignore: [popupRef]
}
也可以传 CSS 选择器:
ts
{
ignore: [
'.arco-trigger-popup',
'[data-ignore-outside]',
]
}
也可以同时排除多个元素:
ts
{
ignore: [
popupRef,
toolbarRef,
'.another-popup',
]
}
7.5 为什么 Teleport 场景需要 ignore
很多弹出面板会通过 Teleport 挂载到 body:
text
工具栏
└── 图例按钮
body
└── 图例面板
从 DOM 结构看,图例面板并不是图例按钮的子元素。
因此只监听按钮时,点击图例面板可能被误判为外部点击。
通过:
ts
{
ignore: [popupRef]
}
可以告诉 VueUse:
这个 Teleport 面板虽然不在按钮 DOM 内部,但业务上仍然属于内部区域。
7.6 在 Cesium 地图中的价值
某些 UI 组件默认的外部点击能力,可能依赖普通 DOM 的事件传播。
Cesium 画布、第三方地图引擎或者特殊事件处理逻辑,可能导致组件内部的默认外部点击监听不能稳定触发。
onClickOutside 可以在文档层统一监听,并明确指定按钮和面板的范围,因此更适合:
- Cesium 地图
- Canvas
- Teleport 面板
- 多层弹窗
- 第三方地图容器
官方文档:
8. useDebounceFn:函数防抖
8.1 防抖是什么
防抖表示:
高频触发过程中不立即执行,等操作停止一段时间后,只执行最后一次。
例如用户快速输入:
text
地
地图
地图图
地图图层
如果设置 300ms 防抖,那么用户停止输入 300ms 后才真正发起一次搜索。
8.2 基本使用
ts
import { ref, watch } from 'vue'
import { useDebounceFn } from '@vueuse/core'
const keyword = ref('')
const search = useDebounceFn(() => {
console.log('请求搜索接口', keyword.value)
}, 300)
watch(keyword, search)
8.3 常见应用场景
- 搜索框
- 表单自动保存
- 窗口缩放结束后重新计算
- 地图相机移动结束后查询
- 避免按钮连续提交
- 避免短时间重复校验
8.4 注意异步请求问题
防抖只能阻止新的函数被频繁调用,不能取消已经发出的旧请求。
例如:
ts
const search = useDebounceFn(async () => {
const result = await requestSearch(keyword.value)
list.value = result
}, 300)
如果旧请求已经发出,之后新请求先返回,旧请求又后返回,旧数据可能覆盖新数据。
复杂搜索还需要:
- 使用
AbortController - 使用请求序号
- 判断当前 keyword
- 取消上一个请求
官方文档:
9. useThrottleFn:函数节流
9.1 节流是什么
节流表示:
在持续触发期间,每隔指定时间最多执行一次。
ts
import { useThrottleFn } from '@vueuse/core'
const handleMouseMove = useThrottleFn(() => {
console.log('更新鼠标位置')
}, 100)
监听鼠标:
ts
useEventListener(window, 'mousemove', handleMouseMove)
即使浏览器一秒产生几百次鼠标事件,业务代码也只会大约每 100ms 执行一次。
9.2 常见应用场景
scrollmousemoveresize- 地图相机移动
- 拖动位置同步
- 高频坐标计算
- 高频状态上报
9.3 防抖和节流如何选择
| 场景 | 推荐 |
|---|---|
| 搜索框停止输入后查询 | 防抖 |
| 表单停止修改后保存 | 防抖 |
| 窗口缩放结束后重算 | 防抖 |
| 滚动过程中持续更新 | 节流 |
| 鼠标移动过程中显示坐标 | 节流 |
| 地图移动过程中限制刷新频率 | 节流 |
简单记忆:
text
防抖:我只关心最后一次。
节流:我需要持续执行,但不能执行得太频繁。
官方文档:
10. refDebounced 和 refThrottled
如果需要控制的不是一个函数,而是一个响应式变量,可以使用:
refDebouncedrefThrottled
10.1 refDebounced
ts
import { ref, refDebounced } from '@vueuse/core'
const keyword = ref('')
const debouncedKeyword = refDebounced(keyword, 300)
keyword 会立即变化:
ts
keyword.value = '地图'
debouncedKeyword 会在停止变化 300ms 后更新。
可以直接监听:
ts
watch(debouncedKeyword, () => {
search(debouncedKeyword.value)
})
10.2 refThrottled
ts
import { ref, refThrottled } from '@vueuse/core'
const mouseX = ref(0)
const throttledMouseX = refThrottled(mouseX, 100)
即使 mouseX 高频变化,throttledMouseX 也只会按照限制频率更新。
10.3 如何选择函数版和 Ref 版
| 需求 | 推荐 |
|---|---|
| 控制函数执行频率 | useDebounceFn、useThrottleFn |
| 控制响应式变量更新频率 | refDebounced、refThrottled |
第二梯队:布局和 DOM 中常见
11. useWindowSize:浏览器窗口尺寸
11.1 基本使用
ts
import { useWindowSize } from '@vueuse/core'
const { width, height } = useWindowSize()
返回:
ts
width: Ref<number>
height: Ref<number>
11.2 判断屏幕尺寸
ts
import { computed } from 'vue'
const isSmallScreen = computed(() => {
return width.value < 1024
})
11.3 常见应用场景
- 地图工具栏位置计算
- 大屏和桌面使用不同业务逻辑
- 图表重新计算尺寸
- 弹窗最大高度计算
- 判断是否进入紧凑布局
- 根据视口决定渲染数量
11.4 不要滥用
如果只是改变样式,优先使用 CSS 媒体查询:
less
@media (max-width: 1024px) {
.toolbar {
display: none;
}
}
推荐原则:
text
样式响应式
→ 优先 CSS
业务行为响应式
→ 可以使用 useWindowSize
官方文档:
12. useElementSize:元素尺寸
12.1 基本使用
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useElementSize } from '@vueuse/core'
const containerRef = ref<HTMLElement | null>(null)
const { width, height } = useElementSize(containerRef)
</script>
<template>
<div ref="containerRef">
当前尺寸:{{ width }} × {{ height }}
</div>
</template>
12.2 元素尺寸为什么会变化
元素尺寸变化不一定是浏览器窗口变化导致的,也可能来自:
- 左侧菜单展开或收起
- 父容器布局变化
- 文本内容变化
- Tab 切换
- 弹窗尺寸变化
- 拖动分栏
- CSS 样式变化
因此,监听容器尺寸时不能只使用 window.resize。
12.3 常见应用场景
图表自适应:
ts
watch([width, height], () => {
chart.resize()
})
Cesium 自适应:
ts
watch([width, height], () => {
viewer.resize()
})
动态设置列表高度:
ts
const tableHeight = computed(() => {
return Math.max(height.value - 120, 300)
})
12.4 注意循环更新
如果尺寸变化后修改样式,而新样式又导致尺寸变化,可能形成循环。
text
尺寸变化
→ 修改元素高度
→ 元素尺寸再次变化
→ 再次修改高度
因此回调中不要无条件反复修改目标元素尺寸。
官方文档:
13. useResizeObserver:底层尺寸观察
13.1 基本使用
ts
import { ref } from 'vue'
import { useResizeObserver } from '@vueuse/core'
const containerRef = ref<HTMLElement | null>(null)
useResizeObserver(containerRef, (entries) => {
const entry = entries[0]
const { width, height } = entry.contentRect
console.log(width, height)
})
13.2 与 useElementSize 的区别
| 需求 | 推荐 |
|---|---|
| 只需要元素宽高 | useElementSize |
需要响应式 width、height |
useElementSize |
需要完整 ResizeObserverEntry |
useResizeObserver |
| 需要尺寸变化后执行复杂副作用 | useResizeObserver |
| 需要观察多个目标 | useResizeObserver |
| 需要访问不同 box 尺寸信息 | useResizeObserver |
一般优先使用:
ts
useElementSize()
只有需要底层信息时,再使用:
ts
useResizeObserver()
官方文档:
14. onKeyStroke:键盘快捷键
14.1 基本使用
ts
import { onKeyStroke } from '@vueuse/core'
onKeyStroke('Escape', () => {
popupVisible.value = false
})
14.2 监听多个按键
ts
onKeyStroke(['Enter', ' '], (event) => {
event.preventDefault()
submit()
})
14.3 监听组合键
ts
onKeyStroke(
event => event.ctrlKey && event.key === 's',
(event) => {
event.preventDefault()
save()
},
)
Mac 用户通常使用 Command,可以同时判断:
ts
onKeyStroke(
event =>
(event.ctrlKey || event.metaKey) &&
event.key.toLowerCase() === 's',
(event) => {
event.preventDefault()
save()
},
)
14.4 常见应用场景
Escape关闭弹窗Enter提交Delete删除选中元素- 方向键移动
- Ctrl/Cmd + S 保存
- Ctrl/Cmd + Z 撤销
- Ctrl/Cmd + Shift + Z 重做
14.5 注意输入框冲突
ts
onKeyStroke('Delete', (event) => {
const target = event.target
if (
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement
) {
return
}
deleteSelectedFeature()
})
否则用户在输入框中按 Delete,也可能触发删除地图元素。
14.6 注意长按重复触发
浏览器长按键盘时,会连续触发键盘事件。
ts
onKeyStroke('Enter', (event) => {
if (event.repeat)
return
submit()
})
官方文档:
第三梯队:定时任务和浏览器状态
15. useTimeoutFn:延迟执行一次
15.1 基本使用
useTimeoutFn 是可控制、可自动清理的 setTimeout。
ts
import { useTimeoutFn } from '@vueuse/core'
const {
start,
stop,
isPending,
} = useTimeoutFn(() => {
console.log('3 秒后执行')
}, 3000)
15.2 不立即开始
ts
const {
start,
stop,
isPending,
} = useTimeoutFn(
() => {
copiedVisible.value = false
},
2000,
{
immediate: false,
},
)
启动:
ts
start()
取消:
ts
stop()
15.3 isPending
ts
if (isPending.value) {
console.log('定时任务正在等待执行')
}
15.4 常见应用场景
- 延迟关闭提示
- 复制成功状态保持两秒
- 延迟加载
- 延迟隐藏工具栏
- 短暂成功动画
- 防止直接维护 timeout 句柄
官方文档:
16. useIntervalFn:周期执行
16.1 基本使用
useIntervalFn 是可暂停、恢复、自动清理的 setInterval。
ts
import { useIntervalFn } from '@vueuse/core'
const {
pause,
resume,
isActive,
} = useIntervalFn(
() => {
refreshTaskStatus()
},
5000,
{
immediate: false,
},
)
开始:
ts
resume()
暂停:
ts
pause()
判断是否正在运行:
ts
console.log(isActive.value)
16.2 常见应用场景
- 任务状态轮询
- 设备状态刷新
- 倒计时
- 定期刷新地图数据
- 定期检查消息
- 简单心跳
16.3 防止异步任务重叠
下面的代码可能产生重叠请求:
ts
useIntervalFn(async () => {
await refresh()
}, 1000)
如果一次请求需要三秒,下一次计时可能在上一次完成前开始。
可以增加锁:
ts
let refreshing = false
const {
pause,
resume,
} = useIntervalFn(async () => {
if (refreshing)
return
refreshing = true
try {
await refresh()
}
finally {
refreshing = false
}
}, 5000)
16.4 复杂轮询需要额外考虑
- 页面进入后台时是否暂停。
- 请求失败后是否降低频率。
- 用户离开页面后是否停止。
- 登录失效后是否停止。
- 是否允许请求重叠。
- 是否需要指数退避。
- 是否应该改用 WebSocket。
17. useOnline:网络在线状态
17.1 基本使用
ts
import { useOnline } from '@vueuse/core'
const online = useOnline()
模板:
vue
<template>
<a-alert v-if="!online">
当前网络连接已断开
</a-alert>
</template>
17.2 能说明什么
ts
online.value === false
通常说明浏览器检测到设备离线。
但是:
ts
online.value === true
只说明浏览器认为存在网络连接,不代表后端服务一定正常。
可能出现:
- Wi-Fi 已连接,但不能访问互联网。
- 公司网络正常,但后端服务宕机。
- DNS 异常。
- 网关异常。
- Token 已过期。
- 当前接口不可用。
因此,useOnline 适合做离线提示,不适合作为后端健康检查。
官方文档:
18. usePreferredDark:系统深色模式偏好
18.1 基本使用
ts
import { usePreferredDark } from '@vueuse/core'
const prefersDark = usePreferredDark()
它读取系统或浏览器的:
css
prefers-color-scheme: dark
18.2 监听系统主题变化
ts
watch(prefersDark, (dark) => {
console.log(
dark
? '系统偏好深色'
: '系统偏好浅色',
)
})
18.3 系统偏好不等于应用主题
例如:
text
操作系统:深色模式
应用手动设置:浅色模式
此时:
ts
prefersDark.value === true
但应用实际主题仍然可能是浅色。
推荐的主题优先级:
text
用户手动设置
↓ 用户没有设置
系统颜色偏好
↓ 系统无法识别
应用默认主题
如果需要完整控制主题,可以进一步了解:
useDarkuseColorMode
官方文档:
第四梯队:特定交互场景常用
19. useScroll:滚动位置和滚动状态
19.1 基本使用
ts
import { ref } from 'vue'
import { useScroll } from '@vueuse/core'
const containerRef = ref<HTMLElement | null>(null)
const {
x,
y,
isScrolling,
arrivedState,
directions,
} = useScroll(containerRef)
19.2 返回内容
x:横向滚动距离y:纵向滚动距离isScrolling:当前是否正在滚动arrivedState.top:是否到达顶部arrivedState.bottom:是否到达底部arrivedState.left:是否到达左侧arrivedState.right:是否到达右侧directions.up:是否向上滚动directions.down:是否向下滚动directions.left:是否向左滚动directions.right:是否向右滚动
19.3 触底加载
ts
watch(
() => arrivedState.bottom,
(arrived) => {
if (arrived)
loadMore()
},
)
还应增加加载锁:
ts
watch(
() => arrivedState.bottom,
async (arrived) => {
if (!arrived || loading.value)
return
loading.value = true
try {
await loadMore()
}
finally {
loading.value = false
}
},
)
19.4 回到顶部
ts
y.value = 0
19.5 确保容器可以滚动
less
.scroll-container {
height: 500px;
overflow: auto;
}
如果元素没有固定高度或没有 overflow,可能监听不到预期的容器滚动。
19.6 高频操作配合节流
ts
const handleScroll = useThrottleFn(() => {
updateToolbarPosition(y.value)
}, 100)
watch(y, handleScroll)
官方文档:
20. useClipboard:剪贴板
20.1 基本使用
ts
import { useClipboard } from '@vueuse/core'
const {
copy,
copied,
text,
isSupported,
} = useClipboard()
复制:
ts
await copy('项目编号:123456')
20.2 显示复制状态
vue
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
const { copy, copied } = useClipboard()
function copyProjectId() {
copy('project-001')
}
</script>
<template>
<a-button @click="copyProjectId">
{{ copied ? '已复制' : '复制项目编号' }}
</a-button>
</template>
20.3 绑定响应式来源
ts
const projectUrl = computed(() => location.href)
const { copy } = useClipboard({
source: projectUrl,
})
调用:
ts
copy()
20.4 注意事项
- 剪贴板 API 通常要求 HTTPS。
- 本地开发环境通常也可使用。
- 某些操作必须由用户点击触发。
- 浏览器可能拒绝权限。
- 读取剪贴板比写入限制更多。
- 使用前可以检查
isSupported。
ts
if (!isSupported.value) {
Message.warning('当前浏览器不支持剪贴板操作')
return
}
await copy(value)
官方文档:
21. useMouse:响应式鼠标坐标
21.1 基本使用
ts
import { useMouse } from '@vueuse/core'
const {
x,
y,
sourceType,
} = useMouse()
读取:
ts
console.log(x.value, y.value)
21.2 sourceType
sourceType 可以用于判断位置数据来自:
- 鼠标
- 触摸操作
- 其他指针来源
如果只想监听鼠标:
ts
const { x, y } = useMouse({
touch: false,
})
21.3 常见应用场景
- 地图坐标提示
- 跟随鼠标的 Tooltip
- 自定义十字准星
- 绘图工具
- 调试鼠标位置
- 鼠标附近显示操作按钮
21.4 元素内部坐标
useMouse 更偏向全局鼠标位置。
如果需要某个元素内部的相对坐标,可以考虑:
ts
useMouseInElement()
21.5 注意性能
鼠标移动事件频率非常高,不要每次变化都执行昂贵计算:
ts
watch([x, y], expensiveCalculation)
推荐配合节流:
ts
const updatePosition = useThrottleFn(() => {
updateMapCoordinate(x.value, y.value)
}, 50)
watch([x, y], updatePosition)
官方文档:
22. useDraggable:让元素可拖动
22.1 基本使用
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from '@vueuse/core'
const panelRef = ref<HTMLElement | null>(null)
const {
x,
y,
style,
} = useDraggable(panelRef, {
initialValue: {
x: 100,
y: 100,
},
})
</script>
<template>
<div
ref="panelRef"
class="floating-panel"
:style="style"
>
可拖动面板:{{ x }},{{ y }}
</div>
</template>
<style scoped>
.floating-panel {
position: fixed;
}
</style>
style 通常会生成类似:
css
left: 100px;
top: 100px;
22.2 常见应用场景
- 地图浮动面板
- 可移动工具条
- 小型调试窗口
- 悬浮视频窗口
- 临时辅助窗口
22.3 它不是完整拖拽系统
复杂需求仍然需要自己处理:
- 拖动边界
- 防止拖出屏幕
- 吸附效果
- 碰撞检测
- 列表排序
- 跨容器拖放
- 拖放数据
- 移动端手势冲突
- 键盘可访问性
- 拖动手柄
例如简单边界限制:
ts
const maxX = computed(() => {
return windowWidth.value - panelWidth.value
})
复杂列表拖拽应该考虑专门的拖拽方案,而不是只使用 useDraggable。
官方文档:
第五梯队:网络能力,使用前需要考虑项目架构
23. useFetch:响应式 Fetch 请求
23.1 基本使用
useFetch 是对浏览器原生 fetch 的响应式封装。
ts
import { useFetch } from '@vueuse/core'
const {
data,
error,
isFetching,
execute,
abort,
} = useFetch('/api/projects', {
immediate: false,
}).get().json()
手动请求:
ts
await execute()
取消请求:
ts
abort()
23.2 常见能力
useFetch 能提供:
- 响应式
data - 响应式
error isFetching- 手动执行
- 取消请求
- URL 改变后重新请求
- 请求前拦截
- 响应后拦截
- 创建预配置版本
23.3 响应式 URL
ts
const projectId = ref('001')
const url = computed(() => {
return `/api/projects/${projectId.value}`
})
const {
data,
isFetching,
} = useFetch(url).get().json()
23.4 在已有项目中不要随意替换请求层
很多项目已经有统一 API 请求层,里面会处理:
- Token
- 基础地址
- 请求头
- 错误码
- 登录失效
- 消息提示
- 参数转换
- 响应解包
- 请求日志
- 权限异常
这种情况下,不应在页面里随意使用 useFetch 绕开现有请求封装。
适合使用 useFetch 的情况:
- 简单原型
- 独立工具
- 公开接口
- 不经过业务网关的请求
- 项目决定基于
useFetch建立统一封装
它不是 Axios 的直接增强版,而是原生 Fetch 的响应式封装。
官方文档:
24. useWebSocket:WebSocket 连接
24.1 基本使用
ts
import { useWebSocket } from '@vueuse/core'
const {
status,
data,
send,
open,
close,
ws,
} = useWebSocket('wss://example.com/ws')
24.2 发送消息
ts
send(JSON.stringify({
type: 'subscribe',
projectId: 'project-001',
}))
24.3 接收消息
ts
watch(data, (message) => {
if (!message)
return
try {
const payload = JSON.parse(message)
console.log(payload)
}
catch {
console.warn('WebSocket 消息格式错误')
}
})
24.4 自动重连
ts
useWebSocket(url, {
autoReconnect: {
retries: 3,
delay: 1000,
},
})
24.5 心跳
ts
useWebSocket(url, {
heartbeat: {
message: 'ping',
interval: 30_000,
},
})
具体心跳消息必须符合后端协议,不能默认认为服务端一定接受字符串 ping。
24.6 VueUse 负责什么
VueUse 主要负责:
- 创建连接
- 维护连接状态
- 发送消息
- 保存最后一条消息
- 手动打开和关闭
- 基础自动重连
- 基础心跳
- 组件卸载时关闭连接
24.7 业务仍然需要负责什么
- 消息类型定义
- JSON 解析
- 数据校验
- Token 和鉴权
- 订阅与取消订阅
- 消息顺序
- 消息去重
- 断线后恢复订阅
- 后端心跳协议
- 多页面是否共享连接
- 连接失败提示
24.8 data 通常只是最后一条消息
ts
data.value
通常表示最近收到的一条消息,不是完整消息列表。
如果需要消息历史:
ts
const messages = ref<unknown[]>([])
watch(data, (message) => {
if (!message)
return
messages.value.push(JSON.parse(message))
})
如果消息很多,还要限制数组长度:
ts
if (messages.value.length > 1000)
messages.value.shift()
官方文档:
第六梯队:响应式增强和状态组织
25. useRefHistory:历史记录、撤销和重做
25.1 基本使用
ts
import { ref } from 'vue'
import { useRefHistory } from '@vueuse/core'
const form = ref({
name: '',
description: '',
})
const {
history,
undo,
redo,
canUndo,
canRedo,
clear,
} = useRefHistory(form, {
deep: true,
clone: true,
capacity: 20,
})
25.2 使用撤销和重做
ts
undo()
redo()
clear()
模板:
vue
<a-button
:disabled="!canUndo"
@click="undo"
>
撤销
</a-button>
<a-button
:disabled="!canRedo"
@click="redo"
>
重做
</a-button>
25.3 deep 的作用
对于对象内部修改:
ts
form.value.name = '新名称'
一般需要:
ts
{
deep: true
}
否则可能无法完整记录对象内部变化。
25.4 clone 的作用
对象是引用类型。
如果多个历史记录指向同一个对象,后续修改可能影响旧记录。
因此对象历史通常需要:
ts
{
clone: true
}
25.5 capacity 的作用
ts
{
capacity: 20
}
表示只保留有限数量的历史记录,防止内存不断增长。
25.6 常见应用场景
- 表单撤销
- 编辑器撤销和重做
- 地图绘制历史
- 图层样式修改历史
- 配置面板回退
- 轻量操作记录
25.7 大对象要谨慎
下面的场景可能非常消耗内存:
ts
const hugeGeoJson = ref(...)
如果每次修改都完整深拷贝 GeoJSON,历史记录会快速占用大量内存。
大型地图数据更适合记录"操作命令":
text
添加点 A
删除线 B
修改面 C 的属性
而不是每次保存整个数据快照。
25.8 相关能力
useDebouncedRefHistoryuseThrottledRefHistoryuseManualRefHistory
例如输入框连续输入时,可以用防抖历史,避免每输入一个字符都生成记录。
官方文档:
26. createGlobalState:轻量全局共享状态
26.1 基本使用
ts
// use-map-ui-state.ts
import { ref } from 'vue'
import { createGlobalState } from '@vueuse/core'
export const useMapUiState = createGlobalState(() => {
const activePanel = ref<string | null>(null)
const toolbarVisible = ref(true)
function closePanel() {
activePanel.value = null
}
return {
activePanel,
toolbarVisible,
closePanel,
}
})
组件 A:
ts
const { activePanel } = useMapUiState()
activePanel.value = 'legend'
组件 B:
ts
const { activePanel, closePanel } = useMapUiState()
console.log(activePanel.value)
组件 A 和组件 B 得到的是同一份状态。
26.2 常见应用场景
- 简单 UI 状态
- 当前打开的地图工具
- 多个组件共享窗口状态
- 多个组件共享网络状态
- 不值得创建 Pinia store 的简单状态
26.3 不适合替代 Pinia 的场景
以下情况通常优先使用 Pinia:
- 核心业务状态
- 用户信息
- 权限信息
- 复杂 actions
- 大量接口请求
- 需要 DevTools 调试
- 需要明确模块边界
- 需要插件、持久化或日志
官方文档:
27. createSharedComposable:共享一个 Composable 实例
27.1 解决什么问题
假设一个组合函数内部创建了多个监听器:
ts
function useWindowMonitor() {
const size = useWindowSize()
const online = useOnline()
return {
...size,
online,
}
}
多个组件分别调用时,可能创建多份响应式作用域和监听。
可以使用:
ts
import { createSharedComposable } from '@vueuse/core'
export const useSharedWindowMonitor =
createSharedComposable(useWindowMonitor)
多个组件调用:
ts
const {
width,
height,
online,
} = useSharedWindowMonitor()
它们会共享同一个 composable 结果。
27.2 常见应用场景
- 多组件共享窗口尺寸监听
- 多组件共享鼠标位置
- 多组件共享网络状态
- 共享同一个媒体查询
- 避免重复创建昂贵观察器
- 共享同一个订阅来源
27.3 与 createGlobalState 的区别
| 能力 | 侧重点 |
|---|---|
createGlobalState |
共享一份响应式状态 |
createSharedComposable |
共享整个 composable,包括状态和副作用 |
28. createInjectionState:组件树局部共享状态
28.1 解决什么问题
它封装了 Vue 的:
ts
provide()
inject()
适合只在某棵组件树内共享状态。
例如:
text
地图页面
├── 图层面板
├── 图例面板
├── 底图面板
└── 属性面板
这些组件需要共享地图上下文,但地图页面之外不应该访问。
28.2 定义上下文
ts
import { computed, ref } from 'vue'
import { createInjectionState } from '@vueuse/core'
const [
useProvideMapContext,
useMapContext,
] = createInjectionState((projectId: string) => {
const selectedLayerId = ref<string | null>(null)
const hasSelection = computed(() => {
return selectedLayerId.value !== null
})
return {
projectId,
selectedLayerId,
hasSelection,
}
})
28.3 父组件提供状态
ts
useProvideMapContext(props.projectId)
28.4 子孙组件使用状态
ts
const mapContext = useMapContext()
if (!mapContext)
throw new Error('必须在地图上下文中使用')
mapContext.selectedLayerId.value = 'layer-001'
28.5 与其他状态方案的区别
| 需求 | 推荐 |
|---|---|
| 全应用复杂业务状态 | Pinia |
| 全应用简单共享状态 | createGlobalState |
| 共享 composable 及其副作用 | createSharedComposable |
| 只在某棵组件树内共享 | createInjectionState |
官方文档:
VueUse 常见能力选型速查表
| 我想实现的功能 | 优先使用 |
|---|---|
| 注册 DOM 或窗口事件 | useEventListener |
| 点击面板外部关闭 | onClickOutside |
| 长期保存用户偏好 | useLocalStorage |
| 保存当前标签页临时状态 | useSessionStorage |
| 搜索输入停止后查询 | useDebounceFn |
| 限制滚动或鼠标回调频率 | useThrottleFn |
| 延迟更新响应式变量 | refDebounced |
| 限制响应式变量更新频率 | refThrottled |
| 监听浏览器窗口大小 | useWindowSize |
| 获取某个元素宽高 | useElementSize |
| 获取底层尺寸变化信息 | useResizeObserver |
| 监听快捷键 | onKeyStroke |
| 延迟执行一次 | useTimeoutFn |
| 周期执行或轮询 | useIntervalFn |
| 判断浏览器可能离线 | useOnline |
| 获取系统深色偏好 | usePreferredDark |
| 获取滚动状态 | useScroll |
| 复制文本 | useClipboard |
| 获取鼠标位置 | useMouse |
| 拖动悬浮面板 | useDraggable |
| 简单响应式 HTTP 请求 | useFetch |
| 管理 WebSocket 生命周期 | useWebSocket |
| 实现撤销和重做 | useRefHistory |
| 轻量全局共享状态 | createGlobalState |
| 共享一个 composable 实例 | createSharedComposable |
| 组件树局部共享状态 | createInjectionState |
推荐学习顺序
如果刚接触 VueUse,建议按照以下顺序掌握:
text
1. useEventListener
2. onClickOutside
3. useLocalStorage / useSessionStorage
4. useDebounceFn / useThrottleFn
5. useWindowSize / useElementSize
6. useTimeoutFn / useIntervalFn
7. onKeyStroke
8. useScroll / useClipboard
9. useMouse / useDraggable
10. useOnline / usePreferredDark
11. useFetch / useWebSocket
12. useRefHistory
13. createGlobalState / createSharedComposable / createInjectionState
使用 VueUse 的判断思路
开发过程中,如果原生代码中出现下面这些 API,可以先检查 VueUse 是否已经提供对应封装:
text
addEventListener
removeEventListener
ResizeObserver
IntersectionObserver
setTimeout
setInterval
localStorage
sessionStorage
navigator.clipboard
navigator.onLine
matchMedia
fetch
WebSocket
mousemove
scroll
例如:
text
addEventListener
→ useEventListener
ResizeObserver
→ useElementSize / useResizeObserver
setTimeout
→ useTimeoutFn
setInterval
→ useIntervalFn
localStorage
→ useLocalStorage
点击元素外部
→ onClickOutside
窗口尺寸
→ useWindowSize
剪贴板
→ useClipboard
但 VueUse 不是"看到原生 API 就必须替换"。
以下情况可以继续使用原生 API:
- 逻辑非常简单。
- 使用范围非常局部。
- 不需要响应式状态。
- 不涉及组件生命周期。
- 项目已经有统一封装。
- VueUse 封装反而让逻辑更难理解。
核心原则是:
VueUse 用来减少重复的生命周期管理和浏览器 API 样板代码,而不是为了使用 VueUse 而使用 VueUse。