更新时序要由本地最新状态决定

更新时序要由本地最新状态决定

# 更新时序要由本地最新状态决定

结论

你在同一条「编辑 → 保存 → 回写」链路上踩了三次坑,形状完全一样: 「谁最新」这个判断,被交给了「谁最后返回 / 谁最后执行」。 而异步世界里,返回顺序和用户操作顺序无关。 解法统一:把时序显式记下来 —— 版本号、捕获的实体 id、光标状态 —— 更新点一律读它,不读环境。

三处证据

出处症状被误用的「当前值」显式时序
过期响应不能覆盖当前编辑内容刚写的字被回退晚返回的旧请求带着旧正文本地输入版本号 +1,返回时比对
延迟保存要捕获创建时的实体编辑 A 的内容写进了 B计时器触发时的「当前活跃 tab」创建任务时捕获 note.id 并传参
流式更新要合并字段而非替换对象思考内容瞬间被清空「最后一个元素」被整体赋值{ ...prev, content } 保留同对象其它字段
编辑器显示不能脱离真实光标状态源码与渲染结果同时出现固定的渲染规则,不看光标显示由「本地光标位置」决定

旁证:编辑即时反馈与落盘延迟分离 —— 它给出的是这条链路的正确分层:内存即时、落盘延迟、 边界 flush。三者都是在这个分层之下才会显形的时序问题。

综合判断

  1. 「最后发生的」≠「最新的」。 三个 bug 都在同一个前提下成立:把「回调执行的那一刻的共享可变状态」 当成「用户想要的最新状态」。请求乱序、debounce 跨对象、流式增量替换,都只是这个前提的三种触发方式。

  2. 这类 bug 全部静默。 不报错、不 crash,只是内容被覆盖、被清空、被写到别的对象上。 它们的时间窗口往往极窄(只在一帧内、只在有思考流的请求里),所以第一反应都是「偶发」。

  3. 可迁移的动作:任何异步 / 延迟 / 流式更新点,问两句 —— 「这里读的『当前值』是谁写的?它和我要更新的那个对象一定是同一个吗?」 答不干净,就在创建任务时把身份(id / 版本号 / 光标位置)显式捕获下来带进回调。 推论:加载态(null)不是「不用处理」,而是最需要 flush 的时刻。

与既有 synthesis 的关系

  • 分层要落到运行时才有效 —— 讲「分层要有人执行」;本条是它在时序维度上的特例: 版本号/捕获的 id 就是那个「执行者」,没有它,分好的内存层与落盘层就会互相污染。
  • 口径先于数值 —— 同源:先确定「这个值属于谁、是哪个时刻的」,再拿它做判断。

为什么这是 synthesis

单看每一页,都是某次前端 bug 的一个修法(加版本号、捕获 id、spread 合并)。 并排之后才看得出它们共享同一个根因假设:用环境状态代替显式时序。 这个形状与框架、业务无关,可以直接带走。

相关