Skip to main content

这个需求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%:

  1. 强制“双重保险”滚动:给AI下死命令:“滚动定位必须在 requestAnimationFrame 里执行,并且在 setTimeout(0) 里再执行一次兜底。”
  2. “高度缓存”暴政:明确告诉AI:“不要用预估高度,必须维护一个 Map 真实高度缓存,每次消息渲染完成后,用 ResizeObserver 强制更新这条消息的真实高度,并同步修正 totalHeight。”
  3. “切换Chat”加版本号:让AI给每个Chat请求带一个 sessionId,滚动回调执行前先检查:“如果当前 sessionId 不等于发起滚动时的 sessionId,直接 return 放弃执行。”

最后送你一句治本的话:“让AI写虚拟列表,等于让一个背下所有棋谱的人去参加拳击比赛——它懂规则,但扛不住现实物理引擎的重拳。” 对于这种极度依赖浏览器物理像素异步时序的需求,你必须当那个告诉AI“出左拳”的教练,而不是等着它自己打出“降龙十八掌”。