airi × VueUse useIntersectionObserver 实战:元素可见性监听的 Composable、指令与类型声明详解
【免费下载链接】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-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
在 airi 这类以 Vue 3 + TypeScript 构建、大量依赖 VueUse 可复用组合式函数的项目(工作区通过 pnpm-workspace.yaml 的 catalog 统一锁定@vueuse/core^14.4.0)中,"元素何时进入视口"是懒加载、滚动动画、曝光统计等场景的基础能力。本文围绕仓库内的技能参考文档 useIntersectionObserver.md 展开,完整讲解useIntersectionObserver的 Composable 用法、v-intersection-observer指令用法、全部配置参数与类型声明,并结合 airi 仓库中真实使用原生IntersectionObserver的场景说明其落地价值。
核心定位:它是做什么的
useIntersectionObserver属于 VueUse 的Elements分类(见 SKILL.md 中 Elements 表格),核心职责一句话概括:检测目标元素可见性的变化(Detects changes to a target element's visibility)。
它是对浏览器原生IntersectionObserverAPI 的响应式封装:把"元素是否/多可见"这一异步的、事件驱动的检测结果,变成可以驱动 Vue 响应式状态(ref)的数据源,并自动处理生命周期清理。
Composable 用法:监听模板 ref 并驱动响应式状态
文档给出的标准用法如下(注意 Vue 3.5+ 中useTemplateRef替代了getCurrentInstance().refs取法):
<script setup lang="ts"> import { useIntersectionObserver } from '@vueuse/core' import { shallowRef, useTemplateRef } from 'vue' const target = useTemplateRef('target') const targetIsVisible = shallowRef(false) const { stop } = useIntersectionObserver( target, ([entry], observerElement) => { targetIsVisible.value = entry?.isIntersecting || false }, ) </script> <template> <div ref="target"> <h1>Hello world</h1> </div> </template>要点解析:
- 第一个参数
target接收useTemplateRef('target')的结果。从类型声明看,它支持三种形态:MaybeComputedElementRef(单个响应式元素引用)、MaybeRefOrGetter<MaybeElement[]>(元素数组的 ref/getter)、或两者的数组——也就是说它可以一次监听多个元素,甚至监听集合本身动态变化的元素。 - 第二个参数
callback就是原生IntersectionObserverCallback,签名是(entries, observer) => void。回调里解构取entries[0],用entry.isIntersecting(布尔值,是否相交)判定可见性;entry上还带有intersectionRatio(相交比例)、boundingClientRect、rootBounds、time等字段,可用于更精细的逻辑。 - 返回值解构出
stop,用于手动停止观察;composable 在组件卸载时会自动清理,一般无需手动调用stop,除非你希望在卸载前就释放观察器。
targetIsVisible用shallowRef而非ref是刻意选择:它只存一个布尔值,shallowRef能避免不必要的深层代理开销——这在频繁触发(快速滚动)时是更稳妥的写法。
Directive 用法:v-intersection-observer 指令
如果不想在<script setup>里写完整的 composable,VueUse 提供了@vueuse/components中的vIntersectionObserver指令,直接在模板里绑定回调:
<script setup lang="ts"> import { vIntersectionObserver } from '@vueuse/components' import { shallowRef, useTemplateRef } from 'vue' const root = useTemplateRef('root') const isVisible = shallowRef(false) function onIntersectionObserver([entry]: IntersectionObserverEntry[]) { isVisible.value = entry?.isIntersecting || false } </script> <template> <div> <p> Scroll me down! </p> <div v-intersection-observer="onIntersectionObserver"> <p>Hello world!</p> </div> </div> <!-- with options --> <div ref="root"> <p> Scroll me down! </p> <div v-intersection-observer="[onIntersectionObserver, { root }]"> <p>Hello world!</p> </div> </div> </template>指令绑定值有两种写法:
- 仅回调:
v-intersection-observer="onIntersectionObserver",默认以 viewport 为根、threshold: 0; - 回调 + 选项对象:
v-intersection-observer="[onIntersectionObserver, { root }]",第二个元素即UseIntersectionObserverOptions,上例把root指向另一个容器,表示"以该容器的边界(而非浏览器视口)为参照系"来判断相交——这是做容器内滚动曝光检测的关键参数。
配置参数与类型声明(完整继承)
文档给出了完整的类型声明,这里逐字段展开其含义与取值:
export interface UseIntersectionObserverOptions extends ConfigurableWindow { /** * Start the IntersectionObserver immediately on creation * @default true */ immediate?: boolean /** * The Element or Document whose bounds are used as the bounding box when testing for intersection. */ root?: MaybeComputedElementRef | Document /** * A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections. */ rootMargin?: MaybeRefOrGetter<string> /** * Either a single number or an array of numbers between 0.0 and 1. * @default 0 */ threshold?: number | number[] } export interface UseIntersectionObserverReturn extends Supportable, Pausable { stop: () => void } export declare function useIntersectionObserver( target: | MaybeComputedElementRef | MaybeRefOrGetter<MaybeElement[]> | MaybeComputedElementRef[], callback: IntersectionObserverCallback, options?: UseIntersectionObserverOptions, ): UseIntersectionObserverReturn参数速查表:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
immediate | boolean | true | 创建时是否立即启动观察。设为false可实现"按需启动/暂停"(返回值含Pausable,可与isActive联动) |
root | MaybeComputedElementRef \| Document | viewport | 作为判定参照的滚动容器。传一个容器元素后,相交检测以该容器边界为准,适用于容器内滚动场景 |
rootMargin | MaybeRefOrGetter<string> | '0px' | 参照边界外的偏移,语法与 CSS 的margin相同,如'100px 0px';常用来做"提前 100px 触发"的懒加载或预热 |
threshold | number \| number[] | 0 | 0.0–1.0 之间的单个数或数组,表示触发回调所需的最小相交比例。数组如[0.25, 0.5, 0.75]会在每个比例跨越时都触发回调 |
window | Window(继承自ConfigurableWindow) | 当前 window | 指定观察器所在的窗口对象,SSR 环境下可指向defaultWindow以避免访问不存在的 DOM |
返回对象继承Supportable(含isSupported,SSR 下为false)、Pausable(含isActive),并额外暴露stop()手动停止。
几个值得注意的设计:
root与rootMargin均为响应式类型(MaybeComputedElementRef/MaybeRefOrGetter),意味着可以把一个 ref 传进去,当容器元素在异步渲染后才挂载、或偏移量随布局动态变化时,composable 内部会unref并跟随变化,无需重建 observer。immediate: false+ 返回值Pausable:可以把监听挂起,配合其它状态(如元素尚未创建、页面被锁定)在合适时机再激活。isSupported:在 SSR(Nuxt SSR 渲染阶段)该值为false,composable 会安全地不执行浏览器 API,因此这段代码在 SSR 项目中可以无条件使用。
在 airi 仓库中的印证:原生 IntersectionObserver 的典型场景
当前仓库源码中没有直接以useIntersectionObserver命名的调用(技能文档.agents/skills/vueuse-functions是作为 Agent 开发指南随仓维护的参考集),但"用 IntersectionObserver 做可见性驱动"这一模式在 airi 的文档站组件里有真实落地:ThemedVideo.vue 中手动new IntersectionObserver(handleVisibility, {...})(第 31–58 行附近),在视频进入视口时恢复播放、离开时暂停——这正是useIntersectionObserver抽象所覆盖的场景。
对比一下两条路径:
- 原生写法(ThemedVideo.vue 的做法):需要自己保存
observer引用、在onBeforeUnmount中手动disconnect()、处理组件复用与元素替换; useIntersectionObserver写法:生命周期清理、响应式 target、SSR 守卫都由 composable 承担,回调里只需要写"可见时做什么"。
从源码结构看,airi 的 stage-web、stage-ui 等包均已依赖@vueuse/core/@vueuse/shared(见 apps/stage-web/package.json、packages/stage-ui/package.json),因此任何需要"元素进出视口"驱动行为的组件,都可以按本文模式直接使用,而无需新增依赖。
典型实战映射
| 需求 | 推荐配置 |
|---|---|
| 图片/组件懒加载(提前于视口加载) | rootMargin: '200px 0px',回调中当entry.isIntersecting为 true 时加载资源,并可考虑加载后stop() |
| 曝光/统计(进入视口才计数) | threshold: 0.5,保证元素至少 50% 可见才记一次 |
| 滚动进度/分段动画 | threshold: [0.1, 0.25, 0.5, 0.75, 0.9],在回调中读取entry.intersectionRatio驱动样式 |
| 容器内滚动检测(如侧边栏、虚拟列表视口) | root指向容器 ref,配合rootMargin收窄/扩大触发区 |
| 暂停/恢复监听 | immediate: false+ 返回值isActive控制,或调用stop() |
小结
useIntersectionObserver的价值在于把原生IntersectionObserver的三件麻烦事——异步回调、生命周期清理、SSR 兼容——收敛为一个接收响应式 target 的 composable。掌握threshold/root/rootMargin/immediate四个参数,再配合 Composable 与v-intersection-observer指令两种接入方式,就能在 airi 这类 VueUse 深度集成的项目中覆盖绝大多数"元素可见性"需求。进一步的 API 语义可对照文档末尾指向的浏览器标准IntersectionObserver接口,以及 SKILL.md 中同属 Elements 分类的useElementVisibility(仅追踪视口内可见性)、useResizeObserver(尺寸变化)等邻近能力做选型。
【免费下载链接】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-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考