支付宝小程序实现「滚动收起、停顿展开」底部订阅提示一块钉在屏幕下方的订阅提示滚动时悄悄藏起来松手后再悄悄露出来——需求很短但藏着的细节不少。一、需求场景在支付宝小程序里有一块固定在屏幕下方的订阅提示区域希望达到这样的交互用户主动滑动页面时提示区域隐藏用户停止滑动后提示区域重新显示。效果如下图所示动图演示滚动与停顿时的状态切换二、思路拆解把需求翻译成技术语言其实要做的事情就是两件监听页面滚动滚动发生时把区域隐藏监听滚动停止停止后再把区域显示回来。听起来很简单但支付宝小程序其实是各大小程序都一样并没有原生提供滚动结束事件。翻官方文档唯一与滚动相关的 API 是事件名触发时机回调参数onPageScroll页面滚动时触发scrollHeight页面总高度、scrollTop滚动距离顶部高度官方文档示意图 这个回调只告诉你现在滚到哪里了并不会告诉你用户什么时候停下来。所以核心问题变成了怎么自己识别用户停手了业界主流方案有两种节流throttle滚动过程中反复触发但保证每隔 N ms 至少执行一次。防抖debounce等用户停止操作 N ms 后才触发一次。我们要的恰好是防抖的语义——“停下来再显示”。所以基本思路就清晰了每次滚动先把区域藏起来并清掉之前的定时器、开一个新的定时器定时器在 N ms 后执行显示动作。因为只有真正停下来时定时器才不会被清掉所以定时器触发的那一刻就是用户停手的时刻。三、代码实现下面把 axml、js 一次给全方便直接照着抄。3.1 模板axml只需要用a:if把这块区域包起来控制显示与隐藏即可!-- pages/demo/index.axml --viewclasspage!-- 页面正文 --viewclasscontent.../view!-- 底部订阅提示区域 --viewclasssubscribe-reminda:if{{subscribeRemindVisible}}text打开通知第一时间收到新内容/text/view/view对应的 acss可选.subscribe-remind{position:fixed;left:0;right:0;bottom:0;padding:20rpx 32rpx;background:#ffffff;box-shadow:0 -4rpx 12rpxrgba(0,0,0,0.06);}3.2 原作者的原始实现jsdata 里准备三个状态Page({data:{subscribeRemindVisible:true,// 是否展示底部区域布尔值isScrolling:false,// 标记是否处于滚动中timer:null,// 定时器句柄},onPageScroll(){// 正在滚动 → 立刻隐藏区域并把滚动中标记置为 truethis.setData({subscribeRemindVisible:false,isScrolling:true,});// 清掉上一轮可能还没执行的定时器if(this.data.timer){clearInterval(this.data.timer);}// 起一个 100ms 的轮询定时器this.data.timersetInterval((){// 第一次执行100ms把标记清回 false// 第二次执行200ms若标记依然为 false说明这段时间没有新的 onPageScroll// 即用户真的停了 → 显示区域if(!this.data.isScrolling){clearInterval(this.data.timer);this.setData({subscribeRemindVisible:true});}// 每次 tick 末尾都重置标记给下一次比较用this.setData({isScrolling:false});},100);},});这段代码的核心思路就是**“两段式轮询”**第 100ms 那个 tick把isScrolling置成false第 200ms 那个 tick如果isScrolling还是false中间没有新事件进来就显示区域。只有真正停下来时定时器才能跨过这两轮、把区域显示出来。⚠️ 这版写法能 work但有两个不优雅的地方每 100ms 都执行一次setData({ isScrolling: false })会反复触发脏检查和渲染开销偏大isScrolling其实只是个内部标记没必要塞进data挂在this上就好。下面给一版等价的、更干净的实现推荐直接用这版。3.3 推荐实现更干净的等价写法Page({data:{subscribeRemindVisible:true,},onPageScroll(){// 滚动中先清掉旧定时器把区域藏起来if(this._tipTimer)clearTimeout(this._tipTimer);this.setData({subscribeRemindVisible:false});// 等用户停手 200ms 后再把区域展开// 注意句柄挂在 this 上即可不要写进 data避免多余渲染this._tipTimersetTimeout((){this.setData({subscribeRemindVisible:true});this._tipTimernull;},200);},});思路完全一样——“滚动 → 清定时器 隐藏 → 重新起一个定时器停 N ms 后再显示”——但好处很明显只用了一个setTimeout没有isScrolling中间态可读性更高定时器句柄挂在this._tipTimer不进 data不会触发额外渲染状态变更只有显示 / 隐藏两种对外接口更稳定。四、几个容易踩的坑onPageScroll触发频率非常高。实测滚动一次屏幕会触发几十到上百次千万不要在里面做重活请求、复杂计算。本文的方案里只做clearTimeoutsetData一个布尔值是安全的。别用setInterval用setTimeout。setInterval一旦开启就会一直执行需要手动清除setTimeout触发一次就结束跟停手再显示的语义更吻合。停顿时间 100~200ms 比较合适。太短会把滑动中短暂的停顿误判为停止太长会让用户觉得卡。data 里别存太多中间态。isScrolling只是个标记没必要存进 data不进 data 就不会触发脏检查渲染更轻。上面推荐实现里就直接挂在this上。五、小结整个需求拆开来看就三步onPageScroll触发即隐藏—— 监听滚动滚动时清掉旧的setTimeout起一个新的—— 防抖新定时器到点了再显示—— 识别停手。代码总共不到 20 行关键是想清楚什么时刻才算停手——它就是定时器能跑完的那一刻。所有没有提供scrollEnd事件的小程序/Web 容器都可以用这个套路解决。如果本文对你有帮助欢迎点赞 / 收藏 / 评论三连支持一下有更好的实现方案也欢迎留言交流