
前端做多了都会遇到这么个需求让用户选城市而且是多选。界面上一堆省份点开省份露出下面的城市勾选几个顶部还有个已选区域能展示也能删除。光听描述这功能不难可实际埋进项目里从数据结构到交互细节每一步都能给你塞点“惊喜”。尤其当我拿到一个交接过来的旧组件发现它只适配了H5逻辑还写得特别死要移植到微信小程序里跑顺便还得支持分组多选顿感手上的活儿没那么轻松了。这篇文章就围绕“城市多选分组组件优化”这件事把我在实际优化过程中涉及的思路、数据转换流程、渲染性能处理和交互细节完整走一遍。项目背景是一个通用的前端组件核心场景是让用户按省份分组勾选城市用在上门服务、同城业务这类需要选具体城市的模块里。代码主要集中在原生JavaScript配合小程序生命周期逻辑未依赖重型框架组件本身的定位就是轻量、高效、好维护。这件事适合所有正在做小程序、或者打算把既有H5组件平滑迁移到小程序生态里的前端开发者参考。1. 内容整体设计与思路拆解1.1 为什么这个组件值得单独优化城市多选在功能形态上像一个简单的树形控件因此很多人在接手时会低估它的复杂度。实际做下来你会发现这块至少有四重麻烦。第一重是数据不规整。后端接口给的往往是城市列表平铺数组可能有省市两级也可能只有城市没有省份或者用地区编码维护层级关系需要先自己拉成树。第二重是交互状态多。整个组件需要同时维护省份的全选、半选状态城市的勾选状态以及已选城市集合的分析和回显。第三重是小程序的运行环境特殊没有真实DOM原生组件的层级机制也跟H5的不完全一样一旦实现方案没设计好遮罩、滚动、事件冒泡都会有奇怪表现。第四重是性能不少设计方案在处理全选与半选时都用了递归遍历如果城市数量上千每点一次都要在树里深挖一遍老旧机型的卡顿体验极其明显。旧组件的问题恰恰是把这几件事全做复杂了。它的数据转换在渲染阶段做每次数据有变动都重新构建一个树而且重计算了所有节点的选中态。组件内部大量使用setData微信小程序里这个接口是昂贵的因为它是整份数据从一个线程传到另一个线程数据量一大界面的反馈就有了肉眼可见的延迟。另一个大问题是它的状态存储太散选中城市和半选节点散落在多个全局变量里排查起来像在玩捉迷藏。优化的总体思路是先解决数据层问题再解决渲染层问题最后集中打磨交互细节。不追求一步到位把组件重构成某种框架专属组件而是让它维持“零依赖、可复制、跨端复用”的轻量特征。1.2 核心优化方向数据层与交互层的双向解耦想明白了之后我把组件的核心架构改成了“单向数据流 派生状态”。原来组件里既能看到数据源又能在同一层看到UI状态维护起来非常难受。现在我把它们拆开基础数据源是城市树的原始结构它只来源于后端返回视图状态是当前每个节点的选中情况、展开情况最后真正要提交给上层业务的结果又是另一个状态——已选城市集合。三者的关系变成了“基础数据源不变视图状态变化引发结果变化”而不是像旧版本那样一切都在一个乱炖的数据源里操作。这件事的核心收获是你不再需要在每一个交互点都去搜索和修改城市树。你只需要维护一份映射表键是城市编码值是选中与否所有交互统一走这份映射。渲染时该组件会简单检查映射表的值然后将结果实时输出到已选列表。这样做的结果就是逻辑复杂度从“图遍历”级别降到了“查表”级别代码量看起来反而少了很多。2. 核心细节解析与实操要点2.1 平铺数据怎么变成省城两级的树不管H5还是小程序后端接口直接给一棵现成的城市树是不常见的更多情况是甩给你一个大数组。比如下面这种结构就能描述一个市级编码归属关系const cityList [ { code: 110100, name: 北京市, provinceCode: 110000, provinceName: 北京市 }, { code: 440100, name: 广州市, provinceCode: 440000, provinceName: 广东省 }, { code: 440300, name: 深圳市, provinceCode: 440000, provinceName: 广东省 }, { code: 330100, name: 杭州市, provinceCode: 330000, provinceName: 浙江省 } ];面对这种数据一个比较常见的做法是循环遍历塞进一个children数组。但要留意有些城市数据会存在子级也就是说树其实不止两级比如“省—市—区”。在处理的时候不应该一刀切死层级而应该抽象成一个支持任意深度转换的函数function buildTree(array, idKey, parentIdKey) { const nodeMap new Map(); const tree []; array.forEach(item { nodeMap.set(item[idKey], { ...item, children: [] }); }); array.forEach(item { const parent nodeMap.get(item[parentIdKey]); if (parent) { parent.children.push(nodeMap.get(item[idKey])); } else { tree.push(nodeMap.get(item[idKey])); } }); return tree; }这样一个函数就兼容了三级、四级甚至更多层级的数据。它还顺带解决了一个很隐蔽的问题旧组件依赖后端给数据时就已经排好序如果父节点在子节点之后出现前端遍历时没建上父子关系城市就会直接丢失。有了Map方案关联完全不受原始顺序影响穿件再乱的数组也能正常关联。2.2 单选与多选的视觉和逻辑边界城市选择器最核心、最容易让用户困惑的就是单选和多选的视觉边界。单选逻辑不需要分组里提供复选框或选中底色之外的复杂反馈多选则要把三种视觉状态一并处理好未选、全选、半选。我参考了一些开源组件的设计后用“勾选图标 分组标题背景块”来实现这个边界。分组标题的左侧有一个自定义样式的框内部三种状态分别是空、对勾、短横线。这三者之间切换遵循下面这些规则城市都没选框是空心。全部选了框是打勾。部分选了框是短横线表示半选。这些状态的判断不能写死在模板里需要抽一个方法每次渲染前根据城市选中映射表算出来。同时要注意不能因为性能问题就放弃半选态的实时反馈半选态直接影响用户下一步操作。点击分组标题只有两种结果要么组内全选要么把组内全部取消勾选。这里面有一个隐藏很深的坑就是“取消最后一个城市时分组状态没变化”。很多人实现时会只在变更项上更新缓存如果当前城市取消后组内还有其他城市选中状态当然还是“半选”如果恰好是最后一个就必须同步把分组状态改回“未选”。这两个路径单独看都没问题但合在一起一不小心就漏了分支。我的组件里用了一个专门的函数做“根据已选集合回算每个分组状态”的操作确保任何入口的变更都会收敛到同一个计算流程。2.3 小程序层级与遮罩层的兼容处理把组件从H5迁到小程序时我遇到的第一个视觉差异是遮罩层和弹层在原生组件面前的层级问题。小程序中有很多原生组件比如地图、视频、canvas它们的层级天然比普通view高遮罩层不一定能盖住它们。但单纯的城市选择组件内部并不会用到这些原生组件所以这个问题在“组件自身里面”并不会出现反而出现在“组件被其他页面元素包裹”时。页面如果同时使用原生区域弹出来之后就会出现内容错位。规避这个问题的方法一般是两条路。一条是把弹层放在小程序的原生root-portal节点里官网方案让弹层直接挂载到页面根节点从物理结构上脱离当前组件的层叠上下文。另一条是使用cover-view但那个限制比较多适应性不强。最终我放弃了对cover-view的依赖采用的是root-portal方案想要跨端的时候再根据平台分别处理。另外还有一个很常见的层级坑是z-index设置的直觉陷阱。H5里一个遮罩层想盖住一个弹层只要把遮罩的z-index调大就行小程序里这种直观操作同样也有效但前提是它们在同一个原生层里。如果遮罩是普通组件弹层是root-portal那么弹层天然就比遮罩高一个层级无论遮罩写多大数字结果都不受控。想要弹层始终在遮罩上就得让两个节点都在root-portal子树上真的别再浪费一下午去调一个不再生效的z-index数字。2.4 通知外界与数据回显组件优化不能只关注自己工作与外界的通信方式也决定了好不好接入。旧版组件的通信方式大约是通过组件内部写有“绑定回调”的引用导致一个组件只有一个固定父亲复用性特别差。我的优化方法是将选中的城市集合统一向外暴露通过一个回调事件通知父页面。组件不关心父页面把这个结果用到哪里也不关心页面其他逻辑。接口变成这样之后大量场景都可以直接复用比如首屏让用户确认服务城市或者在一个已保存信息维护页里回显旧数据。回显数据这块也要讲清楚。用户从旧的已选城市集合进入页面需要重新在分组里把那些城市标成选中状态。这不能靠让用户重新点一遍也不能靠程序遍历一遍城市树去比对。维护一个所选城市编码的Set结构初始化时逐个放入集合然后直接渲染视图状态效率就可以接受。但要注意城市编码的数据类型有的接口给整型有的给字符串统一转换后再放入集合否则“110100”和110100会变成两个不同对象回显就会莫名其妙地少几个城市。微信小程序在回显时还有一个特殊性就是异步渲染。如果初始化时还没把城市树数据准备好就去设置选中集合数据返回后组件可能丢状态。组件内部就需要一个“等待数据就绪再合并初始状态”的机制。我在代码里加了一个isDataReady标志初始数据和用户后续操作产生的数据更新都等这个标志变为true再执行。这个标志让组件看起来“慢热”但实际上避雷了数据时序问题。3. 实操过程与核心环节实现3.1 从需求到组件结构动手之前我给组件画了个大的模块划分按职责分成五个部分城市数据源模块负责拉取接口、转换树状结构、提供查询方法选中状态模块维护已选城市编码集合与分组半选全选状态视图渲染模块渲染分组列表与城市列表、处理滚动位置已选结果模块展示当前已选城市标签和删除操作通信模块向页面抛选中变化事件和初始化完成事件每个模块之间没有直接DOM引用关系唯一沟通的纽带是数据源与状态缓存。这个设计让我在调小程序的时候能快速定位问题。3.2 关键代码数据转换与缓存映射开发中最有价值的几个代码片段这里单独拿出来讲。第一段是原始数据缓存与状态映射的初始化function initCities(rawList) { this._rawList rawList; this._tree buildTree(rawList, code, parentCode); this._selectionMap new Map(); // 键是 code值是 true/false this._initSelection(); // 把回显数据合并进 selectionMap this._syncGroupStatus(); // 全选半选状态计算 }这里的重点是用Map而不是普通对象来当映射表。Map的键可以是任意数据类型不怕整型字符串混用而且它遍历顺序是稳定的这在小程序的setData渲染时可以让列表的展示顺序天然一致。如果你用对象一旦键名是数字形式的字符串某些环境会自动排序你从已选区域看到的城市顺序就可能跟勾选顺序不一致体验上非常奇怪。第二段是分组的全选与半选状态判定。这个函数需要访问selectionMap和cityTree。function checkGroupSelected(provinceItem) { const cities provinceItem.children || []; const selectedCount cities.filter(city this._selectionMap.get(city.code)).length; if (selectedCount 0) return none; if (selectedCount cities.length) return all; return half; }这个函数每次执行时都需要遍历该组城市。为了让它在城市巨多时依然响应快我们可以搞一个按省份编码缓存分组城市数量的结构下次判定时就无需反复取树。这里要说明的是省的名称可能变化但编码不会变组的唯一标识最好使用国家行政编码。城市数量超过500时省这个层级的遍历成本是很低的这些优化细节可以先做缓存不需要一开始就背上“过度优化”的思想包袱。第三段是点击分组标题时的全选取消逻辑function toggleProvince(provinceItem) { const cities provinceItem.children || []; const groupSelected checkGroupSelected.call(this, provinceItem); const nextState groupSelected all ? false : true; cities.forEach(city { this._selectionMap.set(city.code, nextState); }); this._syncGroupStatus(); this._emitChange(); }要注意一点这里的nextState是用“当前是不是联想到全选”来决定的它是一个布尔值不再像旧代码那样用字符串状态做分支判断逻辑清爽不少。3.3 分组折叠动画与滚动定位小程序里做展开和折叠效果最忌直接操作一组view的display属性。因为小程序组件没有v-show这种实时渲染控制直接用display切换会丢失过渡动画用户感觉上是“唰”一下就出来了极其生硬。我的方案是在每个分组标题下挂一个包含所有城市的容器容器的高度通过样式绑定来动态控制展开时绑定实际高度收起时绑定0同时overflow: hidden。不过这个方案有两个小坑第一是手动指定具体高度容易在机型适配时出问题推荐结合wx.createSelectorQuery()去拿实际渲染高度。这是一个异步过程要防止拿到的旧高度缓存。第二是一次只能展开一个分组还是允许多个分组同时展开取决于产品需求。如果你做的是城市选择通常保持“单分组展开”能省去很多滚动位置的焦虑因为用户在长列表里操作时一个分组就够了。不过多分组展开在业务上可能会有对比需求需要自己在点击展开时维护一个展开分组数组。滚动定位也有讲究。当分组很多页面结构超过一屏时用户点击顶部的“已选区域”里的城市标签最好能自动滚到对应分组让用户知道这个城市在哪个省份之下。用scroll-into-view可以轻松解决。但要注意这个属性的值必须对应分组根节点的id并且id不能包含数字开头的字符串否则某些基础库版本就是不识别这种问题排查起来非常费眼睛。3.4 搜索能力如何不影响核心性能城市组件使用过程中一个高频需求是搜索。常见的设计是提供一个搜索框输入城市名拼音或首字母进行过滤。这个功能跟分组多选搭配起来很容易设计成灾难——用户每输入一个字符都要重算整个城市树和勾选状态。优化后的组件里搜索是要接入的但不能改动原有树结构。正确的做法是维护一个“搜索关键词”渲染城市列表时从树里过滤出匹配项匹配的时候不涉及当前选中状态的重算因为结果集合本身是动态的。启动搜索时额外加一个防抖函数避免每敲一个字母就触发一次渲染。城市数量少时防抖简单设300毫秒就好城市上千时建议把过滤逻辑放到wxs里操作小程序有内置支持的过滤器语言在模板层做筛选避免频繁把数据传到逻辑层。这里有个理念要写清楚搜索过滤与分组状态是两件并行的事情过滤不能破坏原有分组树。每次搜索完清空关键词界面要能够恢复原来的分组结构。假如你在搜索过程中直接把原树里没匹配到的城市删掉了清空搜索时还得重新拉数据体验和性能都会很差。4. 常见问题与排查技巧实录4.1 页面下拉穿透与滚动穿透城市多选弹层组件打开后用户滚动城市列表底层页面也跟着滚动这是所有弹层组件都会遇到的问题。H5端处理相对简单给body设置overflow: hidden即可。小程序没有body概念对page元素设置样式也不是每个组件都能生效。我有两种解决思路。第一种是在打开弹层时动态设置页面的page-meta里的page-style把overflow设为hidden关闭时恢复。第二种是直接在弹层外层节点上绑定catchtouchmove禁止滚动事件冒泡。同时如果组件若干个子节点本身需要滚动比如城市列表区则必须让滚动的容器节点绑定自己的滚动事件并调用catch方向判断。使用catchtouchmove有一个反直觉的坑所有子节点的事件都会被拦截包括滚动容器自身的滚动。所以实际写法应该是容器内的滚动节点用bindtouchmove而遮罩背景用catchtouchmove。这样背景区域的滑动不会触发穿透而列表区域依然滚动自如。小程序里禁用滚动的策略因端而异。如果你同时还跑在支付宝小程序上页面滚动穿透的兼容写法又不同优先使用page-meta方案这是跨端兼容性相对较好的做法。4.2 选中状态看不到变化的排查路径组件跑起来后我遇到过一个比较典型的样式问题城市勾选时界面上的对勾图标不显示但底下的已选区域已经出现该城市名。这种问题属于典型的“逻辑已更新视图未刷新”。在小程序里最常出现的原因是没有使用this.setData而是直接改了数据源对象。微信小程序的模板绑定只认setData更新直接修改内部数组并不会触发视图重算。因为我开发时是原生小程序语法只要数据变化点没有统一走状态入口就很容易犯这种错误。优化后的设计每个交互函数末尾都强制调用一次this._refreshView()统一把状态映射转成视图层数据。排查时还有一个位置容易遗漏当我们用Map存选中状态但模板里wx:for循环的是数组对象如果数组对象自身没有selected字段那模板里引用不到状态也会出现“滚动后状态丢失”的假象。这时我采用的做法是给数据的每个城市对象补一个渲染层字段如selected和groupStatus但这并不修改原始数据缓存。这样保证了模型层干净同时视图层拿到完整状态。4.3 分组状态错乱的根因分析有一次测试时发现先全选A省再全选B省然后取消A省里的某个城市B省的分组标题也跟着变成了半选。这个现象一眼看是状态串了。深挖后发现是分组状态缓存用的键写成了省份名称而不同接口来源的省份名称中存在重名或别名差异导致B省的数据顶掉了A省的缓存。修复方案是将所有分组状态缓存的键从名称调整为省份编码。不管后端怎么改名字“省份编码不会变”这个前提是稳的。同时在分组状态重算时也绝不能依赖缓存的引用去更新而要遍历所有省份编码重新计算。虽然多一重循环处理但换来的是状态绝对准确值得。4.4 城市列表长列表卡顿的优化如果某个省份下面的城市特别多比如广东、江苏这种大省展开后渲染出几百条城市单项多少年老的Android机就会出现打开卡顿。旧版组件是直接在模板里写多层嵌套的wx:for去渲染所有城市本身没问题问题是它把已经折叠分组里的城市也渲染出来了只是在视觉上用样式隐藏掉了。优化为“按需渲染”后思路立刻清晰展开哪个分组才渲染哪个分组的城市列表。折叠状态下该组的城市数据是空的。这样做的优点非常显著页面节点数降低90%即使城市总数过千屏幕上可交互的节点也不多。折叠分组的数据清空还附带一个性能好处就是不需要响应半个屏幕外的展示更新。这类方案还需要在展开时重新拉取该组城市数据这会依赖组件内部对城市树结构的快速访问能力。因为我之前做过Map结构缓存所以按编码拉取子级城市是O(1)级别顺滑。4.5 小程序基础库版本差异导致的样式问题写组件时最容易让人崩溃的是在开发工具里一切正常到真机上按钮位置整体错位几个像素。排查原因往往会落到小程序基础库版本不同导致的样式组件支持差异上。比如新版本支持gap旧版本不支持我的分组列表格间距就是用gap实现的结果旧机型上全部挤在一起。要兼容旧版本最好的办法是少用新的CSS特性改用安全的margin或padding方案。因为组件可能就是放在别人的项目里被不同基础库环境使用你没法保证宿主项目用什么版本。如果你在代码里大量刷了新技术特性建议在文档里标明最低基础库版本否则就是给自己埋雷。这里再明确一个事项gap并不是唯一有版本差异的属性还有aspect-ratio、部分CSS变量逻辑等。简单可靠胜过炫技看着不那么新潮但这个组件可以被任何项目直接拿走使用这一点给我的价值感更胜一筹。5. 配套场景选型与组件可扩展性观察5.1 和原生picker对比什么时候自研更划算相信很多前端同行看到这里都会有一个疑问城市选择这件事为什么不用微信小程序自带的picker组件模式直接从modemultiSelector的多列选择器里解决这个思考非常合理但两者应用场景不同。自带picker适合“单选城市”它的操作方式是滚轮不适合在大列表里扫一眼就勾选多个城市的场景。多选城市在移动端本身就是树形结构浏览加勾选的模式自带picker的首屏效率远不如分组勾选高。而且picker的选择结果回显也只能显示一个选中值多选结果列表的展示还需要额外开发还不如自研组件一步到位。反过来如果你的业务只是让用户选一个常驻城市多列联动选择器的代表者picker会是更轻的方案不需要引入一整套自定义组件。选型核心依据是交互心智而不是平台是否有现成能力。5.2 城市数据要不要本地化内置项目的城市列表可以走接口动态下发也可以前端内置一份静态数据。我的组件一开始是接口下发后来发现这个问题在弱网环境里会让组件加载特别慢。城市数据本身很不常变完全可以把一份国标行政编码表内置到组件包里下拉刷新时再拉接口对比一次即可。内置数据后组件初始化不需要等接口回调用户体感非常快。静态数据会带来体积增大的代价一份省市两级名称加编码的文件压缩后通常在几十KB到上百KB不等对小程序包体来说不是不能接受。如果你的项目追求极致包体控制也可以拆出城市数据独立分包在组件需要时再异步加载。前者胜在交互快适合强依赖城市选择的业务后者适合以一个页面为主的轻量级工具型小程序。还有一点值得说不管内置还是接口下发都要在数据结构层做容错。万一后端将来把接口返回从数组改成对象组件也不至于挂掉。我在数据接入层做了两套格式解析既解析数组格式也解析对象格式哪种可用就用哪种处理起来不会给使用者添麻烦。5.3 从城市多选延展到通用分组选择器组件做到后面我发现城市多选只是树形分组多选的特例。树形结构不仅可以放城市还可以放行业类别、商品分类、组织架构等等。把“城市”这个概念抽象成“节点”把“省份”抽象为“分组”组件就有了极大的扩展性。这个抽象过程可以在不改动交互逻辑的前提上完成。初始化时传入节点数据、节点名称字段、节点编码字段所有内部逻辑都基于这两个字段工作。这样组分隔逻辑、全选半选状态、回显流程都是通用的唯一变化的是最终提交数据结构里多了一个层级信息外部接入时需要适配。迁移到下一个业务场景时我只需要换数据源和重新设计展示样式其余逻辑全部复用。这算是我在整个优化过程中最有延展价值的一部分收获。6. 测试与联动验收细节6.1 交互状态的手动铺设场景功能开发完不代表能用完整的测试排序我习惯从最贴近真实用户的操作链路开始。第一次打开组件默认不选任何城市——搜索一个城市——勾选——看已选区域——再折叠分组——重新进入。这整条链路干净跑通之后才能说基础流程没问题。接着铺异常场景一个城市没有归属省份时旧逻辑会把无归属城市丢弃新逻辑会把它挂到一个“其他”分组下避免数据静默丢失。测试时主动在后端造一条无归属数据来验证这个逻辑确保它不会变成幽灵选项。多选的极端状态也要测比如对整个省份多次全选/取消已选城市集合与分组状态必须始终保持一致。这类状态最后的判断标准其实很朴素任何时候已选集合包含某个城市这个城市所在的分组标题就不能显示‘全选’要么钩在半选态要么在未选态。别小看这个标准它能把所有时序性bug都逼出来。6.2 性能数据采集方式组件上线前我拿旧组件和优化版做了对比。测试机选择一台中低端Android设备和一台iPhone旧机型打开一个省份节点数较多的页面记录点击省份展开到首屏城市完全渲染出来的耗时。旧组件在展开大省时平均耗时在260毫秒往上偶尔还有白屏闪断优化后的按需渲染配合轻量数据映射整个展开过程能压在80到100毫秒以内已经无限接近原生组件切换的体感。折叠时再检查内存水位原来的组件在反复展开折叠几十次后页面滚动有明显变卡迹象。新组件因为折叠时释放了子节点数据页面一直保持在低节点负担状态长时间操作也不会出现性能滑坡。性能数据这类事情很难从纯理论上估算最好每次都跑真机实测开发工具里工程师模式的性能面板仅供参考。6.3 回归测试与多端联动验证由于组件需要跑跨端场景我把回归测试分成了三层。第一层是小程序开发者工具里的模拟器快速验证功能第二层是两台真机Android和iOS各一台覆盖基础库版本差异和手势操作习惯第三层是打包到某个测试项目里让另一个同事模拟业务方接入验证组件对外接口说明是否简单明了。这层验证的核心目的是确认外部接入者不需要读源码也能快速用起来。之前有一个版本的组件虽然功能一切正常但回调命名和参数格式没有写进注释导致接手的同事需要反复查看源码才弄清怎么用。后来我专门在组件文件头部写了完整的使用说明和事件参数说明这个习惯直接减少了接入现场一半的问答量。组件写出来是给人用的沟通成本也是工程质量的一部分。7. 个人踩坑总结与心得这次优化做完我对城市多选分组组件的理解加深了不少很多细节认知是平时查文档看不出来的。组件看似是个局部页面模块但要实现得舒服背后的数据流设计、渲染策略以及跨端差异意识缺一不可。如果让我把最值得分享的心得浓缩成几条大概是数据结构和缓存设计先于页面美观把城市树与选中集合分开维护逻辑复杂度会大幅下降。状态变更统一入口任何地方修改选择结果时都调用同一个同步函数可以避免非常隐蔽的串状态问题。小程序多端场景里优先考虑基础库兼容性而不是追逐最新CSS特性。性能优化的关键是减少渲染节点和减少setData频率而不是在单个函数里磨微操。对外接口文档和注释也是组件工程价值的一部分。实际改动里还有一个很细的体验改进就是城市列表选中时的滚动位置保持。用户在多选时往往是从上往下选中途取消某个城市后如果列表意外回到顶部用户会以为数据被重置了实际上只是滚动位置没有保留。我在滚动容器上监听了scroll事件把当前的scrollTop缓存下来视图刷新后强制恢复位置。这个操作在一个大列表里简直能称为“救命稻草”。另外动手之前建议大家先做一件小事打开线上的城市选择页面亲手按真实用户的方式把取消最后一个城市、跨组切换搜索、多端点击等场景全部走一遍。你会发现不少需求文档没有提但必须处理的问题这些问题才是真正拉开普通组件和专业组件差距的地方。组件的优化从来不是动画做得多么炫酷而是那些用户都不会注意到的边界场景都被稳稳地接住了。