- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
导读
useFocusWithin是 VueUse 中用于「响应式追踪一个元素或其任意后代元素是否获得焦点」的核心组合式函数,其行为与 CSS 伪类:focus-within完全对齐。它在表单校验、下拉菜单、输入框组、卡片高亮等需要感知「焦点是否落在一个元素子树内部」的场景中非常实用。读完本文,你将掌握useFocusWithin的完整用法、底层事件机制(focusin/focusout+:focus-within匹配)、可配置参数以及如何在真实 Vue 3 项目中落地。
一、它解决什么问题:焦点状态的「范围感知」
原生 DOM 的focus/blur事件只能告诉你「某个具体元素」是否聚焦,却无法回答一个更常见的 UI 问题:
用户当前正在操作我包裹的这个表单 / 下拉面板 / 输入框组吗?
举个例子:一个包含 4 个<input>的表单(姓名、姓氏、邮箱、密码),你希望「只要其中任何一个输入框获得焦点,就高亮整个表单容器、显示帮助提示或激活提交按钮」。如果用原生事件,你需要给 4 个输入框分别绑定监听;而useFocusWithin只需一行代码,就能把「焦点是否落在整个表单子树内」抽象为一个响应式布尔值。
官方文档(packages/core/useFocusWithin/index.md)明确指出,它就是为了匹配:focus-withinCSS 伪类的行为而设计,常见用例正是「在表单元素上查看其任一输入框当前是否获得焦点」。
二、快速上手:追踪一个表单的焦点状态
文档给出的最小可用示例非常直观:用ref声明目标元素,调用useFocusWithin(target)拿到focused响应式引用,再用watch观察它的变化。
<script setup lang="ts"> import { useFocusWithin } from '@vueuse/core' import { ref, watch } from 'vue' const target = ref() const { focused } = useFocusWithin(target) watch(focused, (focused) => { if (focused) console.log('Target contains the focused element') else console.log('Target does NOT contain the focused element') }) </script> <template> <form ref="target"> <input type="text" placeholder="First Name"> <input type="text" placeholder="Last Name"> <input type="text" placeholder="Email"> <input type="text" placeholder="Password"> </form> </template>这段代码说明三个要点:
target可以是模板 ref、响应式 ref 或组件实例,源码通过 unrefElement 统一解析为真实 DOM 元素($el会被自动解包);- 返回值解构出的
focused是一个ComputedRef<boolean>,由源码中的UseFocusWithinReturn接口定义(见 index.ts); - 只要表单内任意输入框获得焦点,
focused即为true;当焦点离开整个表单子树时才变为false。
仓库自带的真实演示(demo.vue)展示了更完整的模板写法:使用 Vue 3.5+ 的useTemplateRef('target')替代普通ref(),并在界面上实时显示Focus in form的布尔状态。
三、进阶场景:与模板 ref 和条件渲染配合
对于用v-if/v-for动态渲染的目标元素,推荐使用useTemplateRef结合模板 ref 的方式(仓库演示 demo.vue 正是这样写的):
<script setup lang="ts"> import { useFocusWithin } from '@vueuse/core' import { useTemplateRef } from 'vue' const target = useTemplateRef('target') const { focused } = useFocusWithin(target) </script> <template> <form ref="target" class="form-card"> <input type="text" placeholder="First Name"> <input type="text" placeholder="Last Name"> <input type="text" placeholder="Email"> <input type="text" placeholder="Password"> </form> <div class="status"> Focus in form: <strong>{{ focused }}</strong> </div> </template>由于目标元素是响应式解析的(内部用computed(() => unrefElement(target))),即使目标在渲染后才挂载、或中途被替换,监听依然会自动跟随最新元素,无需手动重建组合式函数。
四、参数详解:window 可配置项与边界行为
useFocusWithin(target, options)的第二个参数options实现了 VueUse 标准的ConfigurableWindow接口(见 packages/core/_configurable.ts):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
window | Window | defaultWindow(客户端为window,SSR 下为undefined) | 指定自定义window实例,例如在 iframe、测试环境或 mock 场景中注入 |
两个关键边界行为,均有源码(index.ts)与测试(index.test.ts)双重佐证:
- SSR 安全:当
window不存在(如服务端渲染)或document.activeElement无效时,函数直接返回初始状态{ focused }(恒为false),不会注册任何事件监听,也不会抛错。 - activeElement 无效时恒为 false:测试用例通过
new Proxy(window, ...)模拟document.activeElement === null的场景,验证了即便parent.focus()、child.focus()被调用,focused依然保持false——因为状态追踪依赖useActiveElement提供当前活动元素的有效性前提。
五、源码深度解析:focusin/focusout + :focus-within 的双保险
useFocusWithin的实现非常精巧,值得逐行拆解(完整源码见 packages/core/useFocusWithin/index.ts):
const EVENT_FOCUS_IN = 'focusin' const EVENT_FOCUS_OUT = 'focusout' const PSEUDO_CLASS_FOCUS_WITHIN = ':focus-within' const targetElement = computed(() => unrefElement(target)) const _focused = shallowRef(false) const focused = computed(() => _focused.value) const activeElement = useActiveElement(options) const listenerOptions = { passive: true } useEventListener(targetElement, EVENT_FOCUS_IN, () => _focused.value = true, listenerOptions) useEventListener(targetElement, EVENT_FOCUS_OUT, () => _focused.value = targetElement.value?.matches?.(PSEUDO_CLASS_FOCUS_WITHIN) ?? false, listenerOptions)其核心机制可以拆解为四层:
选用
focusin/focusout而非focus/blur:这两个事件支持冒泡,因此只需在目标元素(如<form>)上监听一次,就能捕获子树内任意后代元素(如<input>)的焦点进出,无需逐个后代绑定。这是整个函数「范围感知」能力的基础。useActiveElement提供活动元素前提:函数调用 useActiveElement 响应式追踪document.activeElement(支持deep: true穿透 shadow DOM 查找深层活动元素),并以此判断当前环境是否具备有效的焦点状态,是前面提到的「activeElement 无效时恒为 false」行为的来源。focusout时的:focus-within双保险:focusout触发时,事件的目标可能是离开焦点的子元素,此时不能简单置为false——因为焦点可能只是从input A移到了同表单的input B。因此源码调用targetElement.matches(':focus-within')做最终判定:只要目标元素此刻仍匹配:focus-within伪类,就说明焦点仍在子树内部,focused保持true。这正是测试用例「后代之间切换焦点时状态持续为 true」(index.test.ts)所验证的行为。useEventListener自动管理生命周期:监听通过 useEventListener 注册,在组件卸载时自动移除监听器,不会产生内存泄漏;passive: true保证监听不阻塞滚动等默认行为。
六、测试用例验证:行为即规范
仓库的 index.test.ts 用五个用例完整定义了该函数的契约,可作为理解行为边界的权威参考:
| 测试用例 | 验证行为 |
|---|---|
should be defined | 函数导出存在性 |
should initialize properly | 初始状态focused为false |
should track the state of the target itself | 目标元素自身focus()后为true,blur()后为false |
should track the state of the targets descendants | 子元素、孙元素(child、grandchild)聚焦同样触发true |
should track the state while the descendants switch focus state | 后代之间连续切换焦点(child→child2→child)时focused持续为true,离开后归false |
should the state of target always be falsy when document.activeElement invalid | mockactiveElement为null时,无论谁聚焦都恒为false |
测试还揭示了两个实现细节:测试 DOM 中所有节点都设置了tabIndex = 0以确保可聚焦;结构上构造了form → div → input的祖孙三层嵌套,专门用于验证「后代任意层级聚焦都能被捕获」。
七、实战组合建议与相关函数
useFocusWithin属于 VueUse core 的 Sensors(传感器)类别,从 packages/core/index.ts 的导出可以看出它与useFocus、useActiveElement、useFocusVisible等函数同族。实际项目中可这样组合使用:
- 焦点高亮表单:
focused为true时给表单容器追加高亮 class,用 CSS 过渡实现视觉反馈; - 下拉/弹层自动关闭:结合
useClickOutside思路,在focused变为false时收起面板; - 键盘导航状态:在
watch(focused, ...)中联动快捷键提示或无障碍 aria 状态; - 与
useFocus对比:useFocus(见 useFocus/index.ts)追踪的是「单个元素自身的 focus/blur」且返回可写的focused(可编程聚焦/失焦),而useFocusWithin追踪的是「元素子树整体」且返回只读的focused——两者恰好互补。
八、注意事项与适用前提
- 仅浏览器环境生效:依赖
focusin/focusout事件与Element.matches(),SSR 下直接返回恒false的初始状态(源码 index.ts 已保证无副作用、不报错); - 返回值为只读:与
useFocus不同,useFocusWithin的focused是ComputedRef<boolean>,只能读取,不能通过赋值来聚焦元素; - iframe 场景:如需在 iframe 内追踪焦点,可传入自定义
window选项; - 无需手动清理:监听器生命周期由
useEventListener自动托管,组件卸载即解除。
结合官方文档、源码实现、测试用例与演示页,useFocusWithin是一个「接口极简、语义清晰、边界严谨」的实用工具:一行调用即可获得整个元素子树的焦点状态,值得在表单、面板与交互组件的状态管理中优先采用。
- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
相关推荐
VueUse useActiveElement 完全指南:响应式追踪当前聚焦元素
VueUse useActiveElement 完全指南:响应式追踪当前聚焦元素 useActiveElement 是 VueUse 中一个轻量的 Elemen
前端VueUse useActiveElement 实战指南:在 Airi 中响应式追踪焦点元素与 Shadow DOM 深度遍历
VueUse useActiveElement 实战指南:在 Airi 中响应式追踪焦点元素与 Shadow DOM 深度遍历 导读 useActiveElem
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染VueUse useElementVisibility 完全指南:在 Vue 3 中精准追踪元素可见性
VueUse useElementVisibility 完全指南:在 Vue 3 中精准追踪元素可见性 useElementVisibility 是 VueUs
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考