编程 VueUse v15 升级笔记:templateRef 被删、timer 换成 scheduler,还有 6 处会改行为的变更

2026-10-20 00:04:32

VueUse v15 升级笔记:templateRef 被移除、timer 换成 scheduler,还有 6 处会改行为的变更

面向已经在生产里用 Vue 3 + VueUse 的项目。下面按「v14 里怎么写 → v15 改了什么 → 升级后看到什么现象 → 怎么改」组织。

项目地址:https://github.com/vueuse/vueuse
文档:https://vueuse.org
v15.0.0 Release(2026-09-16):https://github.com/vueuse/vueuse/releases/tag/v15.0.0

环境门槛:Vue 3.5、Node 22、ESM-only

@vueuse/core@15.0.0 的 package.json 里:

  • vue 要求 ^3.5.0
  • Node.js >= 22(v15 起放弃 Node 20,PR #5553)
  • 自 v13 起 ESM-only(v13.0.0 唯一的破坏性变更就是 drop CJS build,PR #4581),require() 会失败
  • exports 只有 "." 和 "./metadata",构建产物都在 dist/ 下

现象:Node 20 的 CI 镜像装包或跑构建直接失败;老式 CommonJS 服务端做 SSR 时 require('@vueuse/core') 报错。

改法:CI 镜像与本地 .nvmrc 一起升到 22;SSR 侧改成 ESM 或走打包器转译。直接引 dist 子路径的代码,位置在 v14 就变过一次(见下),一并核对。

v15 的六项破坏性变更

1. templateRef 移除(PR #4883)

// v14
import { templateRef } from '@vueuse/core'
const el = templateRef('el')

// v15
import { useTemplateRef } from 'vue'
const el = useTemplateRef('el')

现象:templateRef 导入为 undefined,或在类型检查阶段报「模块没有导出成员」。

改法:换成 Vue 内置的 useTemplateRef()。这也是 v15 把 Vue 下限提到 3.5 的直接原因之一。

2. 放弃 Node.js 20(PR #5553)

见上一节。

3. core:移除废弃的 timer 选项,统一为 scheduler(PR #5564)

受影响 API:useCountdown、useNow、useTimestamp、useTimeAgo、useTimeAgoIntl、useTemporalNow、useElementByPoint、useMemory、useVibrate,以及 useWebSocket 的 heartbeat 选项。

// v14
useNow({ interval: 1000 })
useWebSocket(url, { heartbeat: { interval: 30000 } })

// v15:timer 选项被删,改成 scheduler
// scheduler 的类型是 (cb: Fn) => Pausable,
// useRafFn / useIntervalFn 的返回值可以直接塞进去
// 具体字段名与传参按自己装的版本核对 d.ts

现象:传 interval / immediate 之类的旧选项不报错但完全失效,定时刷新停摆或退化成默认频率;heartbeat 不再发送。

改法:全局搜 interval、immediate、heartbeat 三个关键词,逐个换成 scheduler。这类失效没有运行时告警,靠类型检查兜不住,只能搜。

4. useEventSource:'message' 类型两条路径都会触发(PR #5331)

v15 起,处理 SSE 的 'message' 类型时,onmessage 与 addEventListener('message') 两条路径都会触发。

现象:本来就同时挂了 onmessage 回调、又手动 addEventListener('message', ...) 的代码,会收到两份消息,列表重复追加、计数器翻倍。

const es = useEventSource('/api/sse')

// v15:如果这里再挂一次,与 onmessage 路径叠加
es.value?.addEventListener('message', handler)

改法:二选一,删掉手动监听的重复路径。

5. useIDBKeyval:跨标签页同步变更(PR #5338)

现象:多个标签页之间会互相同步 IndexedDB 里的值。原本假设「只有本标签页写入才触发更新」的逻辑会多跑一轮。

改法:多标签页应用回归一遍写入路径,确认重复渲染、重复提交无副作用。

6. useThrottleFn:trailing 默认值从 false 改为 true(PR #5249)

v15 的签名:

useThrottleFn(fn, ms = 200, trailing = true, leading = true, rejectOnCancel = false)

现象见下文「时序差异」一节。

改法:保留 v14 行为就在第 3 个参数显式传 false。

useThrottleFn(save, 500, false)

v14.0.0(2025-10-22):从 v13 直升会一起撞上的变更

  • 迁移到 tsdown,dist 文件位置变化。直接引 dist 子路径的会断
  • 废弃别名导出,改用原始函数名。具体清单以包内 d.ts 的 @deprecated 标注为准
  • 要求 Vue 3.5
  • computedAsync 默认 flush: 'sync'
  • createSharedComposable 在客户端只返回 sharedComposable
  • firebase 集成升到 firebase v12,nuxt 集成用 Nuxt v4 kit
  • useThrottleFn 对齐传统 throttle 语义(#4923)
  • useClipboard 改用 readonly() 而不是类型断言
// computedAsync:v14 起默认 flush: 'sync'
const state = computedAsync(fn, initial, { flush: 'sync' })

// useClipboard:value 是 readonly 的,直接赋值不再生效
const { copy, copied, text } = useClipboard()
await copy('...')          // 走 copy()
// text.value = '...'      // 不要这么写

现象:dist 子路径导入报模块找不到;别名导入在类型检查时报废弃;computedAsync 的求值时机变化,依赖副作用的链路提前执行;createSharedComposable 的解构取值变成 undefined;对 useClipboard().value 赋值静默无效。返回值与 flush 合法取值按自己版本的 d.ts 核对。

顺手修掉的静默 bug:这几处行为会变,值得回归

  • useFetch:更新的请求发出后忽略旧的成功响应;refetch 成功后忽略旧的 abort 错误(#5574)
  • useWebSocket:忽略被替换掉的旧 socket 消息,显式 close() 后状态同步为 CLOSED(#5573)
  • useResizeObserver:observe() 加了 instanceof Element 判断,Vue 注释节点不再让它崩(#5593)
  • pointer 相关:useDraggable / usePointerSwipe 在 pointercancel 时结束,usePointer 重置 isInside,onLongPress 清理待触发的长按(#5555 / #5587)
  • v15 新增:useWebMCP、useLiveAnnouncer、useTemporalNow(#4793)

前三项属于「以前会出错的路径现在不出了」,如果业务里有兜底代码(比如手动比对请求序号、手动清 isInside),可能变成重复处理。

useThrottleFn 的时序差异

leading 与 trailing 都为 true 时:一段连续事件里,第一次立即执行,节流窗口结束时再补一次。

对搜索输入、自动保存、埋点这类「每次调用都有副作用」的地方,等于末尾多打一次请求。v15 默认就是这个组合,v14 不是。

下一步:useCached 的 deepRefs

当前默认 true,官方在类型注释里写明下个大版本改为 false。介意的地方现在就显式传值。

升级顺序

  1. 先锁 Vue 3.5 + Node 22
  2. 改 import:templateRef → useTemplateRef
  3. 全局搜 interval / immediate / heartbeat 选项
  4. 搜 useThrottleFn 调用,按需补第三个参数
  5. 检查 useEventSource 是否自己又挂了 onmessage 监听
  6. 多标签页应用回归 useIDBKeyval
  7. 最后跑一遍 stale response 相关回归:useFetch / useWebSocket
复制全文 生成海报 VueUse Vue3 组合式函数 前端升级

推荐文章

程序员茄子在线接单