更新时序要由本地最新状态决定
# 更新时序要由本地最新状态决定
结论
你在同一条「编辑 → 保存 → 回写」链路上踩了三次坑,形状完全一样: 「谁最新」这个判断,被交给了「谁最后返回 / 谁最后执行」。 而异步世界里,返回顺序和用户操作顺序无关。 解法统一:把时序显式记下来 —— 版本号、捕获的实体 id、光标状态 —— 更新点一律读它,不读环境。
三处证据
| 出处 | 症状 | 被误用的「当前值」 | 显式时序 |
|---|---|---|---|
| 过期响应不能覆盖当前编辑内容 | 刚写的字被回退 | 晚返回的旧请求带着旧正文 | 本地输入版本号 +1,返回时比对 |
| 延迟保存要捕获创建时的实体 | 编辑 A 的内容写进了 B | 计时器触发时的「当前活跃 tab」 | 创建任务时捕获 note.id 并传参 |
| 流式更新要合并字段而非替换对象 | 思考内容瞬间被清空 | 「最后一个元素」被整体赋值 | { ...prev, content } 保留同对象其它字段 |
| 编辑器显示不能脱离真实光标状态 | 源码与渲染结果同时出现 | 固定的渲染规则,不看光标 | 显示由「本地光标位置」决定 |
旁证:编辑即时反馈与落盘延迟分离 —— 它给出的是这条链路的正确分层:内存即时、落盘延迟、 边界 flush。三者都是在这个分层之下才会显形的时序问题。
综合判断
-
「最后发生的」≠「最新的」。 三个 bug 都在同一个前提下成立:把「回调执行的那一刻的共享可变状态」 当成「用户想要的最新状态」。请求乱序、debounce 跨对象、流式增量替换,都只是这个前提的三种触发方式。
-
这类 bug 全部静默。 不报错、不 crash,只是内容被覆盖、被清空、被写到别的对象上。 它们的时间窗口往往极窄(只在一帧内、只在有思考流的请求里),所以第一反应都是「偶发」。
-
可迁移的动作:任何异步 / 延迟 / 流式更新点,问两句 —— 「这里读的『当前值』是谁写的?它和我要更新的那个对象一定是同一个吗?」 答不干净,就在创建任务时把身份(id / 版本号 / 光标位置)显式捕获下来带进回调。 推论:加载态(
null)不是「不用处理」,而是最需要 flush 的时刻。
与既有 synthesis 的关系
- 分层要落到运行时才有效 —— 讲「分层要有人执行」;本条是它在时序维度上的特例: 版本号/捕获的 id 就是那个「执行者」,没有它,分好的内存层与落盘层就会互相污染。
- 口径先于数值 —— 同源:先确定「这个值属于谁、是哪个时刻的」,再拿它做判断。
为什么这是 synthesis
单看每一页,都是某次前端 bug 的一个修法(加版本号、捕获 id、spread 合并)。 并排之后才看得出它们共享同一个根因假设:用环境状态代替显式时序。 这个形状与框架、业务无关,可以直接带走。
相关
- 过期响应不能覆盖当前编辑内容 / 延迟保存要捕获创建时的实体 / 流式更新要合并字段而非替换对象
- 编辑器显示不能脱离真实光标状态
- 对照才产生信息
- 真源要按维度显式指定 —— 同源:没有显式记录时,「最后写入 / 最后返回」就会冒充权威