先说结论:uni-app 里做热搜词横向滚动,marquee 就别碰了,老老实实用 scroll-view + scroll-left + requestAnimationFrame 这套组合,才能在 H5、App、小程序全端都能跑得顺。

marquee 的坑,踩过就不想再提
marquee 标签在 iOS 上直接不渲染,支付宝小程序也是,部分安卓 WebView 还看心情。uni-app 编译时更是直接把它忽略掉,真机跑起来就是一串静止的文字,毫无滚动效果。那些说"复制粘贴 marquee 就能用"的方案,上线后必然翻车。
正确的做法是用 scroll-view 配合 scroll-left 手动驱动横向滚动,再用 requestAnimationFrame 控制帧率,这样全端都能平滑运行。
有几个关键点必须注意:
- scroll-x 要设为 "true",white-space 要设为 nowrap,否则文字换行或宽度塌陷,滚动直接失效
- 子元素(每条热搜词)必须用 display: inline-block,不能只靠 text 标签撑开宽度
- scroll-left 必须是 number 类型,初始值写成 "0" 会导致后续 += 1 变成字符串拼接,变成 "01"、"011"
- 父容器要设固定宽度,比如 750rpx,scroll-view 加上 overflow: hidden
别再 setInterval 了,试试 requestAnimationFrame
用 setInterval 每 16ms 加 1px,在低端安卓机或微信小程序里很容易掉帧、跳词,甚至卡死。尤其当页面有其他动画或接口请求时,定时器会严重偏移,效果惨不忍睹。
requestAnimationFrame 是唯一跨端稳定的节奏控制器——它绑定浏览器重绘周期,iOS 和小程序 WebView 都支持,而且不会被 JS 主线程阻塞。
几个实践经验:
- 滚动速度别写死像素值,按容器宽度比例算,比如每秒滚动 0.15 * 容器宽度,这样能适配不同屏幕
- 重置时机不是等滚到末尾,而是当 scrollLeft >= 总内容宽度 - 容器宽度时,立刻设为 0,避免停顿感
- 用户 touchstart 时必须 cancelAnimationFrame 暂停,松手后恢复,否则手指拖拽时还在后台疯狂滚动
数据更新时,滚动不能断、不能跳
后台推送新热搜,比如"#高考数学#"顶替"#端午出行#",如果直接 this.hotList = newList,scroll-view 会瞬间重排、滚动位置归零,用户看到的就是"闪一下",体验崩坏。
真正可行的方案是双缓冲列表:维持两份相同结构的数据副本,更新时只替换其中一份,通过 v-if 切换渲染源,视觉上无缝衔接。
- 不要用 v-for 直接遍历原始数组,改用计算属性返回拼接后的双倍数组,比如 [...list, ...list]
- 更新数据后,用 this.$nextTick(() => { this.resetScroll(); }) 确保 DOM 重排完成后再重置 scroll-left
- 每条热搜词加唯一 :key,比如 item.id 或 item.text + timestamp,避免 Vue 复用节点导致样式错乱
- 若需高亮"正在上升"词条,别用 class 绑定布尔值,改用 style 动态注入 color 和 transform: scale(1.05),更可控
真机报错:Cannot read property 'clientWidth' of null
这个错误不是代码写错了,是组件还没挂载好就去读 DOM 尺寸。典型场景是在 onLoad 里调用 this.$refs.scrollView.$el.clientWidth —— 此时 scroll-view 还没生成真实节点。
必须等视图真正就绪:优先用 onReady 钩子,再配合 this.$nextTick 获取尺寸;如果热搜数据来自接口,要在 setData 后再调一次 $nextTick。
- 计算单条热搜宽度时,别用 getBoundingClientRect(),它依赖真实 DOM,小程序里可能返回 0;改用 uni.getSystemInfoSync().windowWidth * 0.25 这类估算值,误差可接受
- iOS 上 scroll-view 的 scroll-left 有 1px 像素对齐问题,建议每次递增步长设为 1.2 而非整数,避免卡顿
- 支付宝小程序对 scroll-left 更新频率有限制,超过 60fps 会降频,所以别盲目追求高帧率,30fps 已经足够流畅
滚动本身不难,难的是让每一条热搜词在不同设备、不同网络延迟、不同更新频率下,始终保持着"刚刷出来就自然滑入视野"的节奏感——这要求你把尺寸计算、帧率控制、DOM 就绪判断、数据更新同步全都拆开揉碎,各自兜底。
