Cell单元格组件开发与性能优化实战经验分享

皇甫怡平 组件 阅读 665
赞 37 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个月在做一个移动端的配置管理后台,需求是展示大量结构化的数据条目,每条包含标题、描述、状态、操作按钮等。UI 设计稿里用的是类似 iOS 设置页那种一行一行的单元格样式,产品经理直接叫它“Cell”。一开始我以为随便套个现成的组件库就行,结果一翻文档发现:主流 UI 库(比如 Vant、Ant Design Mobile)的 Cell 组件要么太重,要么定制性差,尤其在动态内容和交互反馈上卡得很死。

Cell单元格组件开发与性能优化实战经验分享

折腾了两天,最后决定自己写一个轻量级的 Cell 组件。核心诉求就三点:支持任意 slot 内容、点击反馈要快、能灵活控制左右布局。反正也不复杂,无非是 div 套 div,关键是怎么把坑避开。

核心代码就这几行

先上基础结构,其实真没多少东西:

<template>
  <div
    class="cell"
    :class="{ 'cell--clickable': onClick }"
    @click="handleClick"
  >
    <div class="cell__left">
      <slot name="left" />
    </div>
    <div class="cell__center">
      <slot name="title" />
      <slot name="desc" />
    </div>
    <div class="cell__right">
      <slot name="right" />
    </div>
  </div>
</template>
.cell {
  display: flex;
  padding: 12px 16px;
  background: #fff;
  border-bottom: 1px solid #eee;
}
.cell--clickable {
  cursor: pointer;
  transition: background-color 0.1s;
}
.cell--clickable:active {
  background-color: #f5f5f5;
}
.cell__left,
.cell__right {
  flex-shrink: 0;
  margin-right: 12px;
}
.cell__right {
  margin-right: 0;
  margin-left: auto;
  text-align: right;
}

逻辑部分也很简单,主要是透传点击事件:

export default {
  props: {
    onClick: Function
  },
  methods: {
    handleClick(e) {
      if (this.onClick) {
        this.onClick(e);
      }
    }
  }
}

就这么点代码,跑起来基本满足需求。左边放图标,中间标题+描述,右边放开关或箭头,很清爽。但问题很快来了——性能。

最大的坑:性能问题

页面要渲染 200+ 条 Cell,每条还带异步加载的状态(比如从 API 拿设备在线状态)。刚开始直接 v-for 渲染,结果滚动卡成 PPT,iOS Safari 尤其严重。我一开始以为是 Vue 的 diff 问题,后来用 Performance 面板一看,发现每次状态更新都触发了整行重排,因为 Cell 里嵌套了太多动态元素。

最头疼的是右边那个状态指示器,可能是个 loading 动画,也可能是个红绿灯图标。之前偷懒直接用 v-if 切换:

<template #right>
  <div v-if="loading">加载中...</div>
  <div v-else-if="online">🟢</div>
  <div v-else>🔴</div>
</template>

结果每次 loadingonline 变化,整个 .cell__right 都要重建,连带文字重排。后来改成用 CSS 控制显示隐藏,避免 DOM 结构变动:

<template #right>
  <div class="status-indicator">
    <span class="status-text" :class="{ hidden: !loading }">加载中...</span>
    <span class="status-icon" :class="{ hidden: loading || !online }">🟢</span>
    <span class="status-icon" :class="{ hidden: loading || online }">🔴</span>
  </div>
</template>
.hidden {
  display: none;
}

这招确实缓解了重排,但 200 条同时更新时还是掉帧。最后咬牙上了虚拟滚动,用 vue-virtual-scroll-list 包了一层。虽然引入了新依赖,但滚动流畅度直接拉满,实测 60fps 稳得很。不过虚拟滚动有个副作用:Cell 高度必须固定,或者至少能预估。我们设计稿里 Cell 高度是 60px,所以还好,加个 estimated-size="60" 就行。

踩坑提醒:这三点一定注意

  • 不要滥用 slot:一开始为了让组件“通用”,我给 Cell 开了 5 个 slot(left、title、desc、extra、right)。结果业务方真的往 extra 里塞了个 mini 图表,导致每行高度不一致,虚拟滚动直接崩了。后来强制规定:只有 title 和 desc 能换行,其他一律单行,超出省略。
  • 点击反馈别用 JS 动画:早期用 JS 改背景色,延迟明显。后来全切 CSS :active,虽然 Android 有些机型有 300ms 延迟,但加个 touch-action: manipulation 就好了。
  • 别在 Cell 里发请求:有同事图省事,在 Cell 组件里直接 created() 里调 API 拿数据。结果 200 个 Cell 同时发请求,浏览器直接限流。后来统一改成父组件批量请求,通过 props 传状态。

最终的解决方案

综合下来,我们的 Cell 组件现在长这样:

<RecycleScroller
  class="scroller"
  :items="items"
  :item-size="60"
  key-field="id"
>
  <template #default="{ item }">
    <MyCell
      :key="item.id"
      @click="handleCellClick(item)"
    >
      <template #left>
        <img :src="item.icon" width="24" height="24" />
      </template>
      <template #title>{{ item.name }}</template>
      <template #desc>{{ item.description }}</template>
      <template #right>
        <StatusIndicator :status="item.status" />
      </template>
    </MyCell>
  </template>
</RecycleScroller>

其中 StatusIndicator 是单独抽出来的无状态组件,只负责根据 props 渲染对应状态,避免 Cell 本身逻辑膨胀。数据全部由父组件通过 API 获取:

// 父组件
async fetchData() {
  const res = await fetch('https://jztheme.com/api/devices');
  this.items = res.data.map(item => ({
    ...item,
    status: 'loading' // 初始状态
  }));
  // 然后再分批查状态,避免一次性请求太多
  this.batchCheckStatus();
}

这样拆分后,Cell 本身变得非常轻,只负责展示,性能问题基本解决。

回顾与反思

现在回头看,这个 Cell 组件其实挺糙的,很多地方能优化。比如虚拟滚动虽然解决了性能,但增加了复杂度;状态指示器用 CSS 切换虽然快,但可访问性(a11y)没考虑,屏幕阅读器可能读不到状态变化。另外,如果未来需求变了,比如 Cell 高度要动态,那虚拟滚动方案就得重做。

但项目时间紧,这种“够用就好”的方案反而最实用。毕竟不是所有场景都需要完美架构,有时候快速交付比技术优雅更重要。这个 Cell 组件上线后没出过性能相关的 bug,用户反馈也说“滑得挺顺”,那就够了。

以上是我个人对这个 Cell 组件的完整踩坑记录,有更优的实现方式欢迎评论区交流。比如你们怎么处理动态高度的虚拟滚动?或者有没有不用虚拟滚动也能扛住 200+ 条的方案?

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论