21 · 深度 JavaScript 交互与状态所有权

适用:多个异步操作相互影响、复杂编辑器、搜索、拖拽、弹层、长任务与跨页状态。只列“点击后调用函数”不足以描述完整交互。

JS-01 · 为交互写状态、事件和不变量

以保存为例,定义 idle、dirty、validating、saving、saved、error、conflict、outcome-unknown;事件包括输入、提交、成功、失败、取消、离开与版本变化。按实际场景裁剪,不要求为了枚举而增加无用状态。

不变量比一串布尔值更容易审查:同一草稿只允许一个主动保存;失败不清空输入;旧版本结果不能覆盖新草稿;界面未确认结果时不显示已保存。

UI 组件展示状态与发出事件;领域控制器决定合法转移;服务适配层执行请求。避免按钮、路由守卫、键盘监听各自维护一份 isSaving。

JS-02 · 按业务语义选择并发策略

任务可用策略不能混淆的事实
搜索建议、筛选查询只应用最新结果,可取消旧读请求取消后仍可能收到旧响应,需检查代次
保存同一草稿串行或合并待保存版本旧保存成功不代表最新输入已保存
支付、发布、创建资源幂等标识、核对结果、按业务限制并发前端取消不能证明服务端未执行
批量上传有界并发、逐项状态部分成功不能重试整批制造重复
本地重计算版本检查、适时调度或 worker阻塞主线程会破坏输入反馈

AbortController 提供取消信号,调用方与被调用任务仍需支持正确语义。参见 MDN AbortController。

计数序号控制“谁还能更新界面”,幂等键控制“操作是否重复执行”,乐观锁控制“是否覆盖更新”,它们不能互相替代。

JS-03 · 防抖不是正确性的保证

防抖降低请求频率,但不能防止乱序响应。按钮禁用降低重复点击,却不能代替服务端幂等。取消计时器只取消本地调度,并不撤销已经发送的写操作。

搜索需要输入法组合态、最新查询标识、请求清理、无结果与旧内容策略。自动保存还需要草稿版本、离线策略、恢复与冲突处理。不要抽一个 debounceEverything 包住全部动作。

JS-04 · 拖拽是可取消的会话

定义起始对象、原位置、指针 ID、候选落点、提交与回滚。指针捕获可以让后续事件持续发给指定元素,见 MDN setPointerCapture。

同时处理 pointerup、pointercancel、lostpointercapture、Escape、对象失效、页面离开与自动滚动。无效落点恢复原状态;提交失败有恢复策略。只记录当前活动指针,避免多指事件相互覆盖。

提供无需拖动的单指针入口,并单独实现适用的键盘操作。触屏上只对必要区域设置合适的 touch-action,不能为了拖拽禁止整个页面正常滚动。

JS-05 · 焦点和快捷键有明确范围

应用快捷键只在其工作区生效;输入、输入法组合态和浏览器常用快捷键需要保护。命令应能通过菜单或按钮发现,而非只能背键盘组合。

模态框维护焦点范围与返回目标;目标已删除时选择邻近对象或稳定标题。多个弹层要有栈与关闭顺序,不应每个组件都独立监听全局 Escape 并同时关闭。

不要依赖固定 setTimeout(300) 等待 DOM。框架更新、焦点转移与布局测量使用对应生命周期或可观察的完成条件。

JS-06 · 撤销、乐观更新与草稿分清楚

撤销记录有业务意义的命令或快照,定义合并粒度、容量和失效规则;不能把每一次键盘事件都当一次撤销。服务端操作是否可以撤销,必须由实际业务能力决定。

乐观更新失败时,只有目标版本仍匹配才回滚,不能把用户之后的修改一并抹掉。草稿持久化明确存储位置、作用范围、版本、过期与隐私;不能简单给每次输入写一个无界 localStorage。

JS-07 · 动态列表与滚动保护阅读上下文

实时流只在用户仍处于跟随模式时自动滚动;用户阅读旧内容时显示新内容入口。虚拟列表使用稳定业务键,维护焦点、测量和定位能力;复用 DOM 不应把输入或焦点转给另一个对象。

拖动滑块、缩放和窗口变化时,区分每帧的临时视觉预览与最终持久化提交。昂贵计算、请求与 DOM 写入有自己的调度,不应每个 pointermove 都执行一遍完整业务链。

JS-08 · 每个副作用都有释放与竞态检查

按组件实例持有请求、计时器、观察器、事件监听和资源 URL。销毁或切换对象后,释放资源并使迟到回调失效。订阅不能因为页面反复进入而重复注册。

测试真实事件顺序:A 慢 B 快、取消后返回、销毁后响应、快速连点、组词回车、拖出边界、弱网重试、保存中换对象、动画被关闭。验证最终业务事实与可见状态,不只检查函数被调用过几次。

教学演示的保存控制器与模拟服务分开;真实服务替换前必须补写操作的结果核对策略。参见共用代码边界和状态恢复。