TT Lab
开始
学习 学习路径 课程

明明在找鲸鱼,却出现了猫

明明在找鲸鱼,却出现了猫

在 TT Lab 中继续学习

一句话总结

搜索框中的输入、发往服务器的请求、界面上显示的结果,是不同时间点的信息。如果把这三者当作同一个字符串来思考,缓慢的过去的响应就会占据最新的界面。

为什么需要它

在月球基地的图书馆里搜索了猫,随即又改成了鲸鱼。请求发往了两个观测站,鲸鱼的响应先到了。界面上刚刚出现鲸鱼,等了很久的猫的响应就到达了。没有错误窗口,HTTP 响应也是成功的,界面上却留下了错误的资料。这与其说是搜索算法的问题,不如说是谁现在有资格改变界面的问题。

在这里,如果禁用搜索按钮,可以把现象掩盖起来。但用户必须能够修正拼写错误或改变主意。是否让用户等待符合产品需求,必须先做出判断。本实验选择的策略是允许新的搜索,并丢弃不再有效的响应。

工作原理

正在输入的 draft 是尚未提交的句子。已提交的请求中,要放入搜索词和提交序号。因为即使再次提交同一个搜索词,它也可能是新的意图。界面状态按下面的方式区分。

状态 界面所知道的内容 需要向用户提供的提示
idle 还没有已提交的搜索 输入搜索词
loading 正在等待某个特定请求的响应 正在等待什么
success 与该请求相符的结果列表 这是哪次搜索的结果
empty 响应成功,但没有结果 修改搜索词
error 请求失败及其原因 重试的办法

如果分别管理 loading: boolean、error: boolean、items: Result[],就可能出现加载和错误同时为开的组合。以 kind 为依据区分的联合类型,会把每种状态应该具有的信息绑在一起。只在 success 中读取 items,只在 error 中读取 message,在渲染函数中也更容易发现矛盾。不过,类型检查并不保证响应的顺序。因为猫和鲸鱼都是类型正确的结果。

先来写一张事件表。编号不是毫秒,而是顺序。

事件 内容 应显示的搜索
1 猫的请求 等待猫
2 鲸鱼的请求 等待鲸鱼
3 鲸鱼的响应 鲸鱼的结果
4 猫的响应 仍然是鲸鱼的结果

最后一行就是必须实现的产品策略。必须能够把“响应成功”这个事实与“现在仍然有效”这个事实分开来解释。

设计的是可能的状态,而不是数据

来比较下面两种状态。示例中的字符串只是为了说明格式而取的样本,实际的响应值会随请求而不同。

type View =
  | { kind: 'idle' }
  | { kind: 'loading'; query: string }
  | { kind: 'success'; query: string; titles: string[] }
  | { kind: 'error'; query: string; message: string };

const waiting: View = { kind: 'loading', query: '별 지도' };
const failed: View = {
  kind: 'error', query: '별 지도', message: '자료실에 연결할 수 없습니다'
};

不是同时打开两种状态,而是从中选出当前的一种状态。如果选择了 error,就必须同时有哪个请求失败了以及提示语句。实际的实验会在此基础上增加 empty。在添加新状态时,不要只修改类型定义就结束,还要一并找出界面文案和状态转换检查。

如果在失败之后重试时转入了 loading,旧的错误语句却依然显示,就说明状态与界面的连接错位了。不要只看创建新请求的函数,而要确认渲染分支读取的是哪些字段。反过来,结果先消失也并不总是正确答案。也有产品会保留之前的结果,同时等待新的结果。如果选择这种策略,就还需要额外的类型来表示“这是之前的结果”以及当前请求的状态。本实验选择清空之前结果的简单策略,专注于竞态。

在现场相遇的样子

不仅是搜索,地址自动补全、按商品选项显示的库存、地图位置变更、按客户显示的详情面板中,都会出现同样的问题。当 FDE 在客户演示过程中遇到界面偶尔变回过去的值的现象时,首先必须同时记录输入时刻和响应时刻。如果只查找失败日志,就会漏掉成功的旧响应。

2026-09-11 查看的 Canonical Web Developer 招聘启事 要求 TypeScript、响应式 UI、可访问性和复杂 UI 的性能,并把大规模 React+TypeScript 经验列为加分项。这个任务把这些要求转化为一个小而可观察的工作。一份 EMEA 招聘启事,并不能保证整个市场的招聘频率或录用结果。

要亲自确认的内容

在实验室的请求队列中,先送达鲸鱼,后送达猫。分别读取输入框和结果标题,并记录在哪个时间点两者出现了错位。请用一句话说明,为什么起始代码的类型检查明明能通过,界面却仍然可能出错。

类型的表示方式请参考 TypeScript 对可辨识联合(discriminated unions)的说明。