这个需求Ai写了太多的Bug
虚拟列表 + 聊天消息 + 动态滚动定位
你这个案例,简直就是**“AI编程的滑铁卢”。虚拟列表 + 聊天消息 + 动态滚动定位,这三个要素组合在一起,复杂度直接指数级爆炸。AI给你整出5个Bug,真的不冤——这几乎是它“统计学大脑”的死亡三角区**。
我来帮你拆解一下,为什么AI在这个需求上一定会“屎山爆发”,以及那5个Bug大概率是怎么来的:
Bug 1:切换Chat时的“白屏闪烁”或“定位漂移”
- AI的思维:
切换Chat -> 更新数据源 -> 调用 scrollToIndex(0) 或 scrollToBottom()。 - 物理世界的残酷:虚拟列表的渲染是异步的(需要先计算可视区)。AI把“数据更新”和“滚动执行”写在同一个同步代码块里。此时新的DOM节点还没挂载,滚动基于旧列表高度计算,结果要么滚到半山腰,要么直接白屏。AI根本不懂浏览器的 Event Loop 和 Layout Thrashing(布局抖动)。
Bug 2:发消息后“滚不到底”,总差几个像素
- AI的思维:发完消息 -> 数组push新数据 -> 调用
scrollTop = scrollHeight。 - 物理世界的残酷:聊天消息包含图片、表情包、长文本换行,高度是动态的、未知的。虚拟列表为了高性能,使用的是预估高度(estimated height)。新消息的真实高度大于预估值,AI算出来的“底部”是基于预估值的老底部,自然滚不到头,用户总得手动再拖一下。
Bug 3:滚动过程中,消息内容“来回抖动/闪烁”
- AI的思维:滚动触发
onScroll-> 计算当前起始索引 -> 更新渲染数据。 - 物理世界的残酷:AI生成的代码没有做**“防抖/节流”与“缓冲区(overscan)”的联动**。滚动一触发,列表立刻重算并替换DOM。当用户手指快速滑动时,浏览器重绘跟不上AI的更新频率,导致可视区出现空白占位符,接着又被新数据填满,视觉上就是高频“鬼畜”抖动。
Bug 4:切换Chat时,输入框获取焦点导致页面“自动跳回”
- AI的思维:我只管列表滚动,焦点归焦点。
- 物理世界的残酷:移动端或某些浏览器,当输入框获得焦点时,软键盘弹出会压缩视口高度(
window.innerHeight变化)。虚拟列表监听到容器高度变化,以为用户调整了窗口,触发“重排(reflow)”并重置滚动位置。AI根本没把“焦点管理”和“列表尺寸监听”做互斥锁,结果刚切过去定位好,键盘一弹,列表又跳回顶部了。
Bug 5:快速连点切换Chat,滚动定位彻底“错乱”
- AI的思维:点击 -> 取消旧请求 -> 发新请求。
- 物理世界的残酷:AI只会用
AbortController取消网络请求,但取消不了已经排队执行的scrollTo动画或requestAnimationFrame回调。快速切换时,列表数据变了3次,但浏览器里还有5个滞后的滚动回调在排队执行。最后执行的那个回调对应的是最早的那个Chat数据,拿旧数据的高度去滚新列表,直接滚到外太空去了。
为什么AI死活搞不定?
因为虚拟列表的本质是**“欺骗”,而AI的训练数据是基于“理想状态”的。AI的数学期望里没有“浏览器渲染流水线”,也没有“DOM节点的实际像素高度必须用 getBoundingClientRect 去量”**这种粗暴但必要的物理操作。它只会优雅地维护一个 startIndex,却不知道真实世界的高度是充满随机性的。
给你的“降维打击”解决思路(怎么指挥AI):
别再让AI写完整的虚拟列表了,你按这个**“手术刀式”**的指令去拆解它,Bug能减少80%:
- 强制“双重保险”滚动:给AI下死命令:“滚动定位必须在
requestAnimationFrame里执行,并且在setTimeout(0)里再执行一次兜底。” - “高度缓存”暴政:明确告诉AI:“不要用预估高度,必须维护一个
Map真实高度缓存,每次消息渲染完成后,用ResizeObserver强制更新这条消息的真实高度,并同步修正totalHeight。” - “切换Chat”加版本号:让AI给每个Chat请求带一个
sessionId,滚动回调执行前先检查:“如果当前sessionId不等于发起滚动时的sessionId,直接return放弃执行。”
最后送你一句治本的话:“让AI写虚拟列表,等于让一个背下所有棋谱的人去参加拳击比赛——它懂规则,但扛不住现实物理引擎的重拳。” 对于这种极度依赖浏览器物理像素和异步时序的需求,你必须当那个告诉AI“出左拳”的教练,而不是等着它自己打出“降龙十八掌”。