ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Airi 实战:VueUse useEventListener 响应式事件监听完整指南

Airi 实战:VueUse useEventListener 响应式事件监听完整指南 Airi 实战VueUse useEventListener 响应式事件监听完整指南【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi导读useEventListener是 VueUse 提供的浏览器事件监听组合式函数composable它把原生addEventListener/removeEventListener的成对注册与销毁封装为一条声明式调用组件挂载时自动注册监听卸载时自动移除无需手动管理清理逻辑。在本仓库Airi自托管的 AI 陪伴应用的 Web 端、Electron 端与共享 UI 组件库中useEventListener被大量用于聊天滚动跟随、颜色选择器拖拽、舞台模型资源回收等场景。读完本文你将掌握useEventListener的全部调用形态默认/显式/响应式目标、单/多事件、单/多目标、清理时机、SSR 兼容写法并结合本仓库源码理解其真实工程用法。一、为什么需要 useEventListener原生写法要求开发者手动配对addEventListener与removeEventListener并保证在组件卸载时移除监听否则会留下事件泄漏listener leak导致回调在组件销毁后仍被触发。useEventListener的核心职责正如其在仓库中的技能文档 useEventListener.md 所描述Register usingaddEventListeneron mounted, andremoveEventListenerautomatically on unmounted.即挂载时注册卸载时自动注销。它还有三重额外的工程价值目标可以是响应式 ref目标变化时自动先注销旧监听、再注册新监听事件与监听器都支持数组一次调用批量订阅多个事件或目标返回清理函数可在任意时刻手动取消监听而不必等到组件卸载。在 VueUse 的函数体系中它属于 Browser 分类见 SKILL.md调用规则为AUTO——即在 Vue/Nuxt 项目中凡是适合的场景都应优先使用它替代手写监听代码。二、基本用法最简单的调用方式与原生addEventListener一致传入目标、事件名和回调函数。import { useEventListener } from vueuse/core useEventListener(document, visibilitychange, (evt) { console.log(evt) })visibilitychange事件会在页面切换到后台/前台时触发常用于暂停/恢复后台任务。回调收到的evt参数具有完整类型推断由于目标是Document事件类型自动收窄为DocumentEventMap中的对应事件对象。三、默认目标省略时监听 window当第一个参数不是目标而是事件名时目标默认为windowimport { useEventListener } from vueuse/core // Listens on window useEventListener(resize, (evt) { console.log(evt) })这是最常用的省略形式因为全局窗口事件resize、scroll、mousemove、keydown、unload等占据了事件监听的很大比例。省略目标后类型推断依然精确——事件名会按WindowEventMap收窄。仓库中的两个典型例证在舞台模型设置 Store 中用默认目标监听页面卸载事件以回收blob:对象 URL见 stage-model.tsuseEventListener(unload, () { revokeStageModelUrl(stageModelSelectedUrl.value) })在颜色选择器中于onMounted内以默认目标监听全局鼠标/触摸移动来实现拖拽跟手见 color-picker.vueonMounted(() { useEventListener(mousemove, handleGlobalMove, { passive: false }) useEventListener(mouseup, handleGlobalEnd) useEventListener(touchmove, handleGlobalMove, { passive: false }) useEventListener(touchend, handleGlobalEnd) })四、响应式目标Reactive Target目标参数不仅可以是Document、Window、HTMLElement等具体对象还可以是一个 ref或 getter。当目标 ref 的值发生变化时useEventListener会自动注销旧目标上的监听、在新目标上注册监听——这正是它区别于手写代码的关键能力。配合 Vue 3.5 的useTemplateRef与v-if/v-else条件渲染可以优雅地实现监听当前活跃元素script setup langts import { useEventListener } from vueuse/core import { useTemplateRef } from vue const element useTemplateRef(element) useEventListener(element, keydown, (e) { console.log(e.key) }) /script template div v-ifcond refelement Div1 /div div v-else refelement Div2 /div /template当cond变化导致 DOM 分支切换时elementref 会先后指向 Div1 与 Div2监听也随之迁移。本仓库的聊天滚动跟随 composable 正是利用了这一点——它把滚动容器本身作为响应式目标传入见 use-chat-history-scroll.ts。容器 ref 变化例如虚拟列表重建 DOM时所有监听自动迁移到新容器无需手动处理useEventListener(container, scroll, () { // 判断是否仍贴近尾部决定是否跟随滚动 }, { passive: true }) useEventListener(container, [wheel, touchmove], () { hasUserScrollIntent true }, { passive: true })更进一步的技巧把computed 派生出的文档对象也作为响应式目标。同一个 composable 中用computed(() container.value?.ownerDocument)得到当前容器所属的 document再监听其selectionchange见 use-chat-history-scroll.tsconst selectionDocument computed(() container.value?.ownerDocument) useEventListener(selectionDocument, selectionchange, () { const selection selectionDocument.value?.getSelection() isSelectionInOlderMessage isOlderMessageItem(findMessageItem(selection?.anchorNode ?? null)) })五、多事件监听Multiple Events第二个参数支持数组一次性为同一目标注册多个事件回调中通过evt.type区分具体事件import { useEventListener } from vueuse/core useEventListener(document, [mouseenter, mouseleave], (evt) { console.log(evt.type) })这在同类交互、统一处理的场景下非常实用。仓库中的聊天滚动 composable 用一个调用同时监听滚轮与触摸移动两种用户滚动意图信号见 use-chat-history-scroll.tsuseEventListener(container, [wheel, touchmove], () { hasUserScrollIntent true }, { passive: true })六、多目标监听Multiple Targets第一个参数同样可以传入目标数组为每个目标分别注册监听import { useEventListener } from vueuse/core const buttons document.querySelectorAll(button) useEventListener(buttons, click, (evt) { console.log(Button clicked) })NodeList也满足Arrayable的约束可直接传入。该能力配合响应式目标使用时可实现监听一组动态变化的元素。七、options 参数被动监听与性能第三个参数options与原生addEventListener的选项一致支持boolean或AddEventListenerOptions选项类型说明captureboolean是否在捕获阶段触发onceboolean仅触发一次后自动移除passiveboolean声明监听器不会调用preventDefault()滚动类事件开启可显著提升滚动性能signalAbortSignal通过AbortController批量取消监听仓库中所有滚动、滚轮、触摸类监听都显式传入了{ passive: true }见 use-chat-history-scroll.ts、use-element-scroll.ts避免浏览器因无法预知preventDefault而降低滚动合成效率。而需要调用preventDefault()的场景如颜色选择器的拖拽需要阻止文本选择与默认触摸滚动则必须显式传{ passive: false }见 color-picker.vueuseEventListener(mousemove, handleGlobalMove, { passive: false })八、手动清理CleanupuseEventListener返回一个清理函数调用后立即注销监听无需等待组件卸载import { useEventListener } from vueuse/core const cleanup useEventListener(document, keydown, (e) { console.log(e.key) }) cleanup() // This will unregister the listener.这一能力让按需切换监听目标变得非常简洁。仓库的useElementScrollcomposable 展示了它的典型用法每次滚动目标变化时先调用上一次返回的清理函数再注册新监听见 use-element-scroll.tsfunction bindScrollTarget(target: HTMLElement | null | undefined) { stopScrollListener.value?.() // 注销旧监听 stopScrollListener.value null scrollOffset.value target?.scrollTop ?? 0 if (!target) return stopScrollListener.value useEventListener(target, scroll, () { scrollOffset.value target.scrollTop updateElementBounds() updateScrollViewportBounds() }, { passive: true }) }随后在watchEffect中随目标变化反复调用bindScrollTarget并在onBeforeUnmount中兜底清理见 use-element-scroll.tsonBeforeUnmount(() { stopScrollListener.value?.() })九、SSR 注意事项如果组件还会在服务端渲染SSR环境中执行直接使用document/window会抛出document is not defined之类的错误因为 Node.js 环境没有这些 DOM API。最稳妥的做法是把监听逻辑放进onMounted钩子——该钩子只在客户端调用能保证 DOM API 可用import { useEventListener } from vueuse/core // onMounted will only be called in the client side // so it guarantees the DOM APIs are available. onMounted(() { useEventListener(document, keydown, (e) { console.log(e.key) }) })注意useEventListener本身不会在服务端抛错当目标为null/undefined时它会安全跳过注册因此真正需要防护的是在 setup 顶层直接引用document/window变量这类写法。仓库中颜色选择器把全局监听放在onMounted中见 color-picker.vue正是这一最佳实践的体现。十、类型系统与重载Type DeclarationsuseEventListener通过 7 组 TypeScript 重载覆盖全部调用形态声明位于 useEventListener.md 的 Type Declarations 一节。核心类型包括interface InferEventTargetEvents { addEventListener: (event: Events, fn?: any, options?: any) any removeEventListener: (event: Events, fn?: any, options?: any) any } export type WindowEventName keyof WindowEventMap export type DocumentEventName keyof DocumentEventMap export type ShadowRootEventName keyof ShadowRootEventMap export interface GeneralEventListenerE Event { (evt: E): void }各重载的适用范围重载目标参数事件类型推断来源典型场景1省略默认windowWindowEventMap全局resize/keydown/unload2WindowWindowEventMap显式指定窗口目标3DocumentDocumentEventMapvisibilitychange/selectionchange4ShadowRoot可空、可数组ShadowRootEventMap自定义元素内部的 Shadow DOM5HTMLElement可空、可数组HTMLElementEventMap元素级scroll/click/pointerover6自定义事件目标InferEventTargetNames由泛型Names extends string推断类 EventTarget 的第三方对象7兜底EventTarget回退为通用Event无法静态推断的通用目标值得注意的通用约束目标参数统一为MaybeRefOrGetterArrayableT | null | undefined——既可以是 ref/getter也可以是数组还可以为null/undefined为 null 时安全跳过这在元素尚未挂载时非常关键事件名为MaybeRefOrGetterArrayableE事件本身也可响应式变化监听器为MaybeRefArrayable...回调自身同样可以是响应式的options为MaybeRefOrGetterboolean | AddEventListenerOptions即passive等选项也能动态更新。回调的this绑定按目标类型精确收窄Window/Document/HTMLElement配合事件名泛型E extends keyof XxxEventMap能让无效事件名在编译期直接报错这也是用 TypeScript 约束 DOM 事件的典型范式。十一、在测试中如何隔离 useEventListener由于useEventListener在组合式函数/Store 初始化时就会注册全局监听单元测试中通常将其 mock 掉避免测试环境真实挂载 DOM 监听。仓库的 stage-model.test.ts 展示了标准做法vi.mock(vueuse/core, async (importOriginal) { const actual await importOriginaltypeof import(vueuse/core)() return { ...actual, useEventListener: vi.fn(), } })通过importOriginal保留vueuse/core其余导出仅将useEventListener替换为vi.fn()。这样在测试 Pinia Store 的逻辑如舞台模型缺失时的回退、Tachie 渲染器路由时unload监听不会真实作用于测试进程。十二、实战总结一个完整的监听场景拆解综合以上能力本仓库的聊天历史滚动 composableuse-chat-history-scroll.ts几乎用遍了useEventListener的全部特性可作为参考样板调用形态用途useEventListener(container, scroll, fn, { passive: true })响应式目标 被动监听判断用户是否贴近消息尾部useEventListener(container, [wheel, touchmove], fn, { passive: true })多事件数组统一收集滚动意图useEventListener(container, keydown, fn)键盘翻页/方向键也视为滚动意图useEventListener(container, [pointerover, pointerout, focusin, focusout], fn)指针与焦点悬停旧消息时暂停自动滚动useEventListener(selectionDocument, selectionchange, fn)以 computed 派生 document 为响应式目标加上颜色选择器的{ passive: false }拖拽场景、useElementScroll的返回清理函数场景、Store 中默认window目标场景可以看到凡是原本需要注册 注销成对出现的事件逻辑useEventListener都能以更简洁、更不易泄漏的声明式方式替代这正是本仓库将它列为AUTO级自动优先选用组合式函数的原因。延伸阅读函数技能总览.agents/skills/vueuse-functions/SKILL.md关联文档原文.agents/skills/vueuse-functions/references/useEventListener.md聊天滚动实战use-chat-history-scroll.ts滚动目标切换实战use-element-scroll.ts拖拽与全局监听实战color-picker.vueStore 中回收资源实战stage-model.ts测试 mock 方式stage-model.test.ts【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表