返回博客

微信小程序自定义 tabBar 切换闪烁:双 setData 竞态根因

某个 C 端小程序上线自定义底部导航后,QA 反馈:每次切换 tab,图标会先亮一下旧高亮、再跳到新高亮,慢网下肉眼可见双闪,快速连点会出现两个 tab 同时高亮一帧。用 DevTools 慢放后确认:不是动画问题,是状态写入顺序的竞态。

现象

  • 点击目标 tab → 当前页面的 tabBar 立刻把目标 tab 高亮(旧页还在屏幕上)
  • 新页面挂载 → 新页面的 tabBar 先渲染默认高亮(index 0),再跳到正确 tab
  • 连击时出现两个 tab 同时高亮的单帧画面

根因

custom: true 的关键特性被忽视了:自定义 tabBar 组件不是全局单例,而是随每个 tab 页面各自挂载一个实例。页面切换 = 旧实例销毁 + 新实例创建,两者互不共享 data

出问题的代码结构:

// custom-tab-bar/index.js  ← 旧实例(当前页面的 bar)
switchTab(e) {
  wx.switchTab({ url: e.currentTarget.dataset.path })
  this.setData({ selected: e.currentTarget.dataset.index }) // ← 写 #1
}
 
// pages/xxx/index.js  ← 目标页的 onShow
onShow() {
  this.getTabBar().setData({ selected: 2 }) // ← 写 #2
}

执行时序:

Frame N   : 旧页面 switchTab() 触发
              写 #1:旧 bar 实例 selected → 目标 index(旧页还在屏 → 用户看到提前高亮)
              wx.switchTab 异步派发 ↓
Frame N+k : 新页面挂载 → 新 bar 实例 data.selected 默认 = 0
              新 bar 先以 selected=0 渲染(第一闪)
              写 #2:onShow setData selected → 目标 index(第二闪:从 0 跳回正确值)

两个不同组件实例,各写各的,中间夹着异步页面切换,顺序无法保证。

坑:在 switchTab 回调里也 setData selected

同时在自定义 tabBar 的 switchTab 方法 各页面的 onShow 里写 selected,等价于两个不同实例各自抢着写同一个状态——它们之间没有协调机制,必然出现竞态闪烁。

setTimeout 延迟旧写入只是掩盖症状,慢设备上仍会触发;把 selected 存入 app.globalData 再在 attached 读取同理,冷启动时还会多一个 stale-on-start bug。

正解

selected 只有一个写入方:页面 onShow。把 tabBar 组件的 switchTab 里的 setData 整行删掉。

// custom-tab-bar/index.js — 修复后
Component({
  data: {
    selected: 0,
    list: [ /* ... */ ]
  },
  methods: {
    switchTab(e) {
      wx.switchTab({ url: e.currentTarget.dataset.path })
      // 不再写 selected,让目标页 onShow 负责
    }
  }
})

每个 tab 页面的 onShow 负责同步自己的高亮索引,并加防御性判空(getTabBar() 在首次挂载时可能短暂返回 undefined):

// pages/home/index.js
onShow() {
  if (typeof this.getTabBar === 'function' && this.getTabBar()) {
    this.getTabBar().setData({ selected: 1 }) // 1 = 本页在 list 中的下标
  }
}

所有 tab 页面用相同模式,selected 值换成各自的固定下标。

正解:selected 所有权归页面 onShow 独占

onShow 是唯一覆盖所有进入路径的生命周期——无论是 wx.switchTabwx.reLaunch、扫码深链、分享回跳、还是从非 tab 页 back 回来,onShow 都会触发。把 selected 的写入收敛到这里,就彻底消灭了竞态的可能。

顺手消除切换时的黑底闪

re-mount 期间新页面容器填满前会短暂露出背景色,若背景色和页面内容色不一致会有黑闪。在 app.jsonwindow + tabBar 以及 app.wxsspage {} 里统一配同一个背景色即可:

// app.json
"window": { "backgroundColor": "#FAFAF9" },
"tabBar": { "backgroundColor": "#FAFAF9", "borderStyle": "white" }
/* app.wxss */
page { background-color: #FAFAF9; }

一句话外卖

微信小程序 custom: true tabBar 每个页面各自挂一个实例——任何跨实例的状态同步都靠 onShow,不要在组件自身的事件回调里抢写。