返回博客

微信小程序 swiper 受控双向绑定引发切后台重进抖动

做一个带自动轮播的活动展示页,卡片进入视野时放大高亮,其他卡片缩小。WXML 写起来很自然:current="{{currentIndex}}" + bindchange 写回,这样随时都能知道"当前是第几张",用来驱动 active class。

首次加载完全正常,autoplay 流畅。直到有一天测试同学反馈:「点右上角 x 关掉小程序,再重新进来,卡片开始左右抖,停不下来。」

现象

  • 首次冷启动:swiper 正常轮播,active 卡片 scale 放大正确。
  • 切后台(微信 home 键 / 点 x 退出)再重新进入:swiper 开始左右双向晃动,卡片 scale 反复闪,autoplay 触发了但永远卡在第 0 张和第 1 张之间来回。
  • 单纯停留在页面不切后台:永远不触发。

根因

微信原生 <swiper>current 属性是**受控(controlled)**的——外部写什么,swiper 内部就跳到什么 index。把它和 bindchange 写回同时使用,形成了一条反馈环:

autoplay 触发滑动
  → bindchange 回调
    → setData({ currentIndex: newIndex })
      → WXML diff,current 属性更新
        → swiper 收到外部 current 变更,执行跳页动画
          → 动画途中下一次 autoplay 触发
            → bindchange 再触发 → 死循环

正常首次加载时,这个环是稳定的(写回的值等于 swiper 自己产生的值,幂等)。但切后台重进时三个放大器同时叠加:

  1. wx:if 闪烁:onShow 重新执行,页面里的 wx:if="{{ready && isVip}}" 守卫在异步数据回来前短暂变为 false,导致 swiper 所在的整个容器节点 unmount → mount,内部 index 被重置。
  2. CSS 过渡叠加.item--activetransition: transform 0.35s 与 swiper 自身 600ms slide 动画并行,两者互相打断。
  3. 异步 setData 干扰:接口回来触发其他字段的 setData,wxml diff 让 current 属性"看似"被重新赋值,swiper 再次触发跳页动画。
坑:current + bindchange 写回构成受控反馈环

<swiper current="{{idx}}" bindchange="onChange"> 加上 onChange 里的 setData({ idx }) 看似无害,实则在 wx:if 闪烁 + 异步 setData 的时序下会彻底失稳。把 transition 时长缩短到 0.1s 也无法根治——必须从根上解开这条双向环。

正解

Fix 1:WXML 删掉 current 属性,让 swiper 自管 index

- <swiper autoplay current="{{currentIndex}}" bindchange="onSwiperChange">
+ <swiper autoplay="{{swiperAutoplay}}" bindchange="onSwiperChange">

swiper 内部自己维护当前 index,外部不再受控。

Fix 2:bindchange 仅用于驱动 active class,不反控 swiper

data: { currentIndex: 0, swiperAutoplay: true },
 
onSwiperChange(e) {
  this.setData({ currentIndex: e.detail.current })
  // currentIndex 只有 view 节点读,swiper 本身不再读它 → 反馈环断开
},

WXML 中子节点保持不变:

<view class="item {{currentIndex == index ? 'item--active' : ''}}">

Fix 3:onHide 暂停 autoplay,onShow 恢复

onHide() { this.setData({ swiperAutoplay: false }) },
onShow() { this.setData({ swiperAutoplay: true }) },

显式控制比依赖原生自暂停更可靠,避免重进时 autoplay 状态不一致。

Fix 4(必要时):wx:if 守卫加 dataReady 防 unmount 闪烁

- <view wx:if="{{isVip && !isBan}}">
+ <view wx:if="{{dataReady && isVip && !isBan}}">

在异步接口成功回调里 setData({ dataReady: true }),让 swiper 容器不在数据未稳定时被反复 unmount/mount。

Fix 5(高频复坑):守卫字段自身不能振荡

套用 Fix 1-4 之后仍然抖动时,检查 dataReady 的计算逻辑——如果它在 onShow 每次执行时会从 true 退回 false,等于把 unmount/mount 的触发权交给了 onShow 里的条件分支。

// 错误:每次 onShow 重新计算,可能 true → false → true
onShow() {
  if (this.data.offlineMemberInfo) {
    resetData.dataReady = true
  } else if (someCondition) {
    resetData.dataReady = false  // ← 会从 true 退回
  }
}
 
// 正确:守卫字段单向,只允许 false → true
onShow() {
  if (!this._hasShown) {
    resetData.dataReady = computeReady(store) // 冷启动走一次完整计算
  } else if (this.data.dataReady === false) {
    if (this.data.offlineMemberInfo || this._memberInfoChecked) {
      resetData.dataReady = true // 热启动只允许升档
    }
  }
  // 其他情况:保持原值,不写
  this._hasShown = true
}
正解:单向化 current,守卫字段只升不降

删掉 current 受控属性,让 swiper 自管 index;bindchange 写回的 currentIndex 只给 view class 绑定读,不反控 swiper。wx:if 守卫字段用实例变量区分冷/热启动,确保一旦变为 true 就不回退。

一句话外卖

原生 swiper 的 current 是受控属性——只要你用它 + bindchange 写回,反馈环就存在,稳不稳看时序;想知道"当前是第几张",让 currentIndex 单向流向 view,别让它流回 swiper。