你做了一个列表容器:顶部有吸顶标题,右侧有渐隐提示,底部有"更多内容"的阴影。
产品提了一个要求:只有当容器内部真的可滚动时,才显示这些提示;并且内容会异步加载、筛选、折叠/展开,容器高度也可能随窗口变化而变化。
问题来了:我怎么知道这个元素当前有没有滚动条?
传统做法的坑:你会不断踩同一类问题
常见实现大多从这几条路开始:
- 监听
scroll:一滚动就更新 UI。 - 首次
mounted/useEffect里测一次:scrollHeight > clientHeight。 - 内容变更后手动再测一次:塞进一堆
nextTick、setTimeout、或自己维护"何时需要重新计算"。
这些方案通常会引出痛点:
- 内容不滚也会变 :异步加载、图片解码、字体加载、折叠/展开,会改变高度,但不一定触发
scroll。 - 容器尺寸也会变 :响应式布局、侧栏收起、窗口缩放,都会改变
clientHeight,你需要额外监听。 - 性能与维护成本高:到处绑定事件、到处分发"刷新",最后很难保证"任何情况下都正确"。
- 框架耦合:Vue/React/原生各写一套,重复劳动。
你真正想要的是:只要元素的尺寸或内容布局变化,就自动重新判断一次,并把结果映射成稳定的样式开关。
这个库解决什么:把"滚动条存在性"变成一个 CSS 开关
当你只需要"有/无滚动条"来驱动 UI 时,最佳形态往往是一个 class:
- 有滚动条 → 添加 class(例如
with-scroll/has-v-scroll) - 没滚动条 → 移除 class
v-scroll-detect 就是把这件事做成了可复用的工具:
- 用
ResizeObserver感知元素布局变化 - 用一次轻量判断(
scrollHeight > clientHeight/scrollWidth > clientWidth)得出结果 - 自动切换 class
- 并提供 Vue 指令、React Hook 与原生核心 API
链接与名称
- 名称:
v-scroll-detect - GitHub:github.com/ccwq/v-scro...
- npm:www.npmjs.com/package/v-s...
下面从"怎么用"开始,再解释"为什么这样实现更稳"。
快速开始
安装
bash
npm i v-scroll-detect
Vue 3:指令(全局注册)
js
import { createApp } from 'vue'
import App from './App.vue'
import vScrollDetect from 'v-scroll-detect'
createApp(App).use(vScrollDetect).mount('#app')
模板里直接用:
html
<div v-scroll-detect>
<!-- content -->
</div>
默认会在任意方向出现滚动条时切换 class:with-scroll。
Vue 3:自定义 class(字符串)
html
<div v-scroll-detect="'has-scroll'">
<!-- content -->
</div>
Vue 3:分别处理纵/横向(对象)
html
<div v-scroll-detect="{ v: 'has-v-scroll', h: 'has-h-scroll' }">
<!-- content -->
</div>
对象模式同时支持别名:
- 纵向:
v或vertical(缺省类名:has-v-scroll) - 横向:
h或horizontal(缺省类名:has-h-scroll)
React:Hook
jsx
import { useRef } from 'react'
import { useScrollDetect } from 'v-scroll-detect/react'
export function Panel() {
const ref = useRef(null)
useScrollDetect(ref, { v: 'has-v-scroll', h: 'has-h-scroll' })
return <div ref={ref}>...</div>
}
原生/框架无关:核心函数
js
import { createScrollDetector } from 'v-scroll-detect/core'
const el = document.querySelector('.my-container')
const detector = createScrollDetector(el, 'has-scroll')
// 手动触发一次检测
detector.check()
// 销毁(解除监听并停止更新)
detector.destroy()
API 设计:用一个 options 覆盖 90% 需求
v-scroll-detect 的核心输入是 options,目标是让"样式开关"足够直观:
undefined:使用默认类名with-scrollstring:任一方向有滚动条时切换该类名object:分别配置纵向与横向类名(支持v/vertical与h/horizontal)
这种设计的好处是:
- 只关心"有没有滚动条"时,一行搞定
- 需要精细 UI(比如仅纵向显示阴影)时,也不用额外写逻辑
实现思路:为什么用 ResizeObserver 更"对味"
滚动条出现/消失,本质上是"内容尺寸"和"容器可视尺寸"的对比结果:
- 纵向:
scrollHeight > clientHeight - 横向:
scrollWidth > clientWidth
难点并不在判断式,而在"何时判断"。
库的做法是:观察元素尺寸变化,一旦变化就安排一次检查,并用 requestAnimationFrame 合并到下一帧执行,避免在频繁布局变化时抖动更新。最终结果就是稳定地 classList.add/remove。
与监听 scroll 相比,它更贴近真实触发源:
- 内容变化、图片加载、字体变化、容器缩放,都能驱动重新判断
- 你不需要在业务代码里维护"何时需要 refresh"
适用与注意事项
- 适用:任何需要根据"是否可滚动"切换样式的场景(阴影、渐隐、边界提示、吸顶边框等)
- 注意:部分平台的"覆盖式滚动条"可能不占据布局空间,但滚动能力仍然存在;这种情况下判断仍然依赖
scrollHeight/clientHeight,一般是可靠的 - 兼容:依赖
ResizeObserver,如需兼容较旧浏览器可考虑引入 polyfill
结语
把"滚动条存在性"抽象成 class 开关后,UI 细节会更干净:业务只写样式与结构,滚动能力的判断交给统一工具完成。v-scroll-detect 的价值就在于:让这个判断在动态布局下也足够可靠,并且跨 Vue/React/原生都能复用。