Web Worker 应仅处理富文本编辑器中与 UI 无关的纯数据操作:Delta 补丁生成/合并/反向应用、历史栈裁剪、锚点快照触发;禁止操作 DOM、选区、焦点,通信须基于可序列化对象,内存优化依赖 transferable 和主线程快照。

用 Web Worker 处理富文本编辑器的撤销重做,核心目标不是“把所有逻辑搬进去”,而是把内存敏感、计算密集、与 UI 无关的历史管理任务剥离主线程——尤其是 Delta 补丁的生成、合并、反向应用和栈裁剪。这样能避免主线程卡顿,防止 GC 频繁打断渲染,同时让大文档(如 10 万节点编辑器)在长时间编辑中保持响应。
哪些操作适合丢进 Worker?
Worker 不该处理 DOM、选区、焦点或任何视图层状态。它只管纯数据:
- Delta 补丁的序列化与反序列化:用户输入后,主线程只传光标位置、操作类型、文本片段等轻量参数;Worker 构建完整 Delta 对象并压入历史栈
- 连续输入的自动合并:Worker 内维护最近一次插入的时间戳和偏移范围,判断是否可聚合成单条 INSERT 补丁(例如 300ms 内连续输入 “hello” → 合并为 1 条,而非 5 条)
- undo/redo 的纯数据还原计算:主线程发来 “执行第 42 步 undo”,Worker 根据当前 state 快照 + 对应 Delta 反向推导出新 state 数据(不含 DOM 结构),再传回主线程
- 栈长度裁剪与锚点快照触发:Worker 定期检查 undoStack 长度,超出 50 条时移除最老 Delta;当检测到合并后 Delta 数达 20 条或出现粘贴操作,主动通知主线程存一次全量快照作为基点
主线程与 Worker 的协作边界要清晰
不能让 Worker 持有 reactive state 或 DOM 引用。通信必须基于可序列化的纯对象:
- 主线程发送:
{ type: 'commit', delta: { op: 'insert', pos: 127, data: 'Vue3' }, selection: { start: 127, end: 131 } } - Worker 返回:
{ type: 'committed', stackSize: 18, needsSnapshot: false }或{ type: 'undoApplied', newState: { content: '...', cursor: 127 } } - 选区、focus、innerHTML 更新、事件派发——全部留在主线程。Worker 只输出“内容该变成什么样”和“光标应在哪”,不碰 DOM
内存优化的关键细节
Worker 本身不解决内存问题,但能让内存压力更可控、更可预测:
-
Delta 不跨 Worker 持久化:每次 new Worker() 开销大,应复用一个长期存活的 Worker;用
transferable传递 ArrayBuffer 或结构化克隆数据,避免重复拷贝 -
快照锚点仍由主线程保存:全量快照体积大(数 MB),不适合频繁 postMessage;Worker 只负责“告诉主线程该什么时候存”,实际
structuredClone(toRaw(state))在主线程执行,并用OffscreenCanvas类似思路——仅当必要时才触发 - 避免闭包式引用泄漏:Worker 内不要通过闭包捕获主线程变量;所有状态用显式 message 参数传递,处理完立即释放局部变量
-
中文输入法兼容靠主线程兜底:compositionstart/end 事件无法在 Worker 监听,所以输入法状态(如正在输入拼音)必须由主线程识别并标注到 commit 消息里,例如
isComposing: true,Worker 据此暂缓合并或标记为待定 Delta
不推荐的做法
有些思路看似合理,实则破坏稳定性或增加复杂度:
- 在 Worker 中解析 HTML 字符串或操作 DOMParser —— 无意义且不可靠,浏览器 API 不可用
- 让 Worker 管理 undoStack/redoStack 的数组引用 —— 无法跨线程共享引用,只能传值
- 用 Worker 做实时 diff(比如每帧比对 innerHTML)—— 耗时、不准、违背“捕获意图”原则
- 把光标恢复逻辑放进 Worker —— Range 和 Selection API 主线程专属,Worker 无法重建选区


















