一、前言
上一篇的函数只碰了 document.title,本篇上强度:useMouse / useWindowSize 要监听 window 事件。这里会沉淀出组合式函数最重要的模板------"注册监听 + 卸载清理"成对出现(011 篇生命周期铁律的函数化版本)。写完这组,浏览器相关的一半组合式函数都是它的变体。
二、useMouse:鼠标坐标的实时追踪
目标 API :const { x, y } = useMouse()------鼠标在页面移动,x/y 实时更新。
实现四步:两个 ref 存坐标 → 一个 update 函数写入 → onMounted 注册监听 → onBeforeUnmount 移除监听:
js
// src/composables/useMouse.js
import { ref, onMounted, onBeforeUnmount } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
// 注意:普通函数(别写箭头函数,行为一致但保持统一风格)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
// 挂载后注册
onMounted(() => {
window.addEventListener('mousemove', update)
})
// 卸载前清理------和注册严格成对!
onBeforeUnmount(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
vue
<!-- 使用 -->
<template>
<p>鼠标位置:x={{ x }},y={{ y }}</p>
<p>在页面上随便移动鼠标试试</p>
</template>
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
三个关键设计:
- update 是具名函数 :注册和移除必须是同一个函数引用 ,如果写成箭头函数字面量传两次(
() => update(e)各建各的),removeEventListener 就删不掉了------这是内存泄漏的重灾区 - 清理放 onBeforeUnmount:组件卸载时移除监听,"开与关成对"(011 篇)在函数内部闭环,调用方完全不用操心
- 钩子写在函数里自动绑实例:哪个组件调用 useMouse,监听就注册/清理到哪个组件的生命周期里(035 篇模型 2 的落地)
【截图位置:鼠标移动、坐标实时刷新的运行效果】
三、useWindowSize:窗口尺寸监听
同款模板换个事件,但多一个初始化细节 :resize 只在窗口变化时触发,初始尺寸要在创建 ref 时就读取:
js
// src/composables/useWindowSize.js
import { ref, onMounted, onBeforeUnmount } from 'vue'
export function useWindowSize() {
// 初始值直接读当前窗口,而不是等第一次 resize
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
function update() {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => {
window.addEventListener('resize', update)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', update)
})
return { width, height }
}
vue
<template>
<p>窗口:{{ width }} × {{ height }}</p>
<p :class="{ narrow: width < 768 }">
{{ width < 768 ? '窄屏布局(模拟移动端)' : '宽屏布局' }}
</p>
</template>
<script setup>
import { useWindowSize } from '@/composables/useWindowSize'
const { width, height } = useWindowSize()
</script>
<style scoped>
.narrow { color: #e67e22; }
</style>
拖动浏览器窗口大小,页面实时响应------响应式布局的 JS 侧逻辑一行接入。对比一下:不初始化就读值的话,页面打开那一刻显示 0 × 0,直到用户手动拖动窗口才正确------"事件触发才更新"的函数,一定要自查初始值。
【截图位置:拖动窗口宽度、布局文案切换的效果】
四、进阶一:带 passive 的滚动监听
滚动场景给监听加 { passive: true }(告诉浏览器"我不阻止默认行为"),滚动性能更好(017 篇埋过的知识点)。封装 useScrollY:
js
// src/composables/useScrollY.js
import { ref, onMounted, onBeforeUnmount } from 'vue'
export function useScrollY() {
const y = ref(0)
function update() {
y.value = window.scrollY
}
onMounted(() => {
window.addEventListener('scroll', update, { passive: true })
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', update)
})
return { y }
}
典型应用:"回到顶部"按钮滚过 300px 才出现:
vue
<template>
<p>当前滚动位置:{{ y }}px</p>
<button v-show="y > 300" class="back-top" @click="toTop">回到顶部</button>
</template>
<script setup>
import { useScrollY } from '@/composables/useScrollY'
const { y } = useScrollY()
function toTop() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
</script>
removeEventListener 要不要也传 passive? 不需要------移除监听只看"类型 + 函数引用",options 里只有 capture 参与匹配。这是新手常纠结的点,记住即可。
五、进阶二:多个组件同时用,监听会重复吗
会------每次调用 useMouse 都创建独立实例:独立的 x/y ref、独立的一对监听。两个组件同时 useMouse,mousemove 上就有两个监听器(各自维护各自的坐标)。
这通常没问题(监听器很轻),但如果逻辑昂贵(比如 scroll 里做重计算),就要单例化------把状态提到函数外面:
js
// 单例版:所有调用者共享同一份 x/y 和同一个监听
const x = ref(0)
const y = ref(0)
let initialized = false
export function useMouseShared() {
if (!initialized) {
window.addEventListener('mousemove', (e) => {
x.value = e.pageX
y.value = e.pageY
})
initialized = true
}
return { x, y }
}
注意单例版无法在组件卸载时清理 (监听是模块级的)------全局都想用、生命周期又贯穿始终的场景才这么写。默认写多实例版,确认昂贵才考虑单例,别过早优化。
六、综合实战:仪表盘小卡片
三个组合式函数同屏协作:
vue
<template>
<div class="dash">
<p>窗口:{{ width }} × {{ height }}</p>
<p>鼠标:x={{ x }},y={{ y }}</p>
<p>滚动:{{ y2 }}px</p>
<button v-show="y2 > 100" @click="toTop">回到顶部 ↑</button>
</div>
</template>
<script setup>
import { useMouse } from '@/composables/useMouse'
import { useWindowSize } from '@/composables/useWindowSize'
import { useScrollY } from '@/composables/useScrollY'
const { x, y } = useMouse()
const { width, height } = useWindowSize()
const { y: y2 } = useScrollY() // 重命名解构:两个 y 不打架(026 篇技巧)
function toTop() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
</script>
体会解构重命名 const { y: y2 } 的作用------多个组合式函数的返回值撞名时,调用方自己排兵布阵,这正是"来源明确"的好处(对比 mixin 的命名冲突:它可没给你重命名的机会)。
【截图位置:仪表盘三项数据实时变化的运行效果】
七、踩坑记录
- removeEventListener 删不掉监听 :注册时传了匿名箭头函数、移除时又新建一个------必须是同一个引用,先声明具名函数再用
- 忘了初始值:useWindowSize 打开页面显示 0------事件触发型数据,创建时就该读一次当前值
- 清理时机放错:清理写进 onUnmounted 也行,但统一用 onBeforeUnmount 与注册钩子对称,成对出现好维护
- SSR 环境直接用 window :服务端没有 window 直接崩------SPA 不管,未来做 SSR 时加
typeof window !== 'undefined'守卫 - scroll 里做重活:滚动事件每秒触发几十次,昂贵逻辑先过防抖/节流------正好是下一篇的主角
八、今日小结
- 组合式函数操作事件的标准模板:ref 存值 → 具名 update → onMounted 注册 → onBeforeUnmount 移除
- 两个自查点:初始值有没有读?监听引用是不是同一个?
- passive 提升滚动性能;单例化是昂贵逻辑的优化手段,默认多实例
- 解构重命名化解多函数撞名------mixin 给不了的自由
下篇预告
防抖和节流------009 篇搜索联想留下的"连续输入发一堆请求"问题,终于到了还账的时候。下一篇 038 亲手封装 useDebounceFn / useThrottleFn,把高频事件的流量阀门装上。