富文本编辑器光标实时同步的核心是封装selection/range api为响应式访问器属性,通过selectionchange等事件驱动读取,结合节点路径定位与节流防抖实现健壮同步。

富文本编辑器中光标位置的实时状态同步,核心不在于“监听光标”,而在于利用浏览器原生的 Selection 和 Range API,结合访问器属性(getter/setter)封装状态读写逻辑,让光标位置像响应式数据一样可追踪、可响应。
用访问器属性封装 Selection 状态
直接操作 window.getSelection() 每次都返回新对象,无法响应式监听。应将其封装为类或响应式对象的访问器:
- 定义
get cursorPosition():内部调用getSelection(),提取当前光标在编辑容器内的偏移(如相对于根节点的文本偏移量range.startOffset,或更健壮的range.getBoundingClientRect()坐标) - 定义
set cursorPosition(pos):接收结构化位置(如 { node, offset } 或 { path, offset }),创建 Range 并调用range.setStart(node, offset)+selection.removeAllRanges()+selection.addRange(range) - 注意:setter 中需确保目标节点仍在 DOM 中,避免因异步更新导致节点被移除而报错
监听关键事件触发 getter 更新
访问器本身不自动执行,需靠事件驱动“读取时机”:
- 绑定
selectionchange(推荐):该事件在用户点击、键盘移动、JS 修改选区后触发,覆盖绝大多数光标变化场景 - 补充
input、keydown(特别是方向键、Home/End)、mousedown:弥补某些浏览器对selectionchange的延迟或遗漏 - 每次事件中主动读取
this.cursorPosition,触发依赖更新(如 Vue 的 computed、React 的 useEffect 依赖项、或自定义订阅机制)
处理富文本 DOM 结构复杂性
纯文本偏移(startOffset)在含内联元素(<strong></strong>、<em></em>)的富文本中易失真。更可靠的做法是:
- 使用
range.startContainer+range.startOffset组合,记录“节点路径”(如通过Array.from(node.parentNode.children).indexOf(node)计算索引) - 或序列化为可还原的定位描述:{ tagName: 'SPAN', indexInParent: 2, textOffset: 5 },便于后续 restore
- 避免依赖
innerText或textContent长度计算位置——它们忽略不可见节点和样式影响,与实际光标位置不一致
同步到外部状态并防抖更新
实时 ≠ 频繁。光标微小移动(如连按方向键)会高频触发,需节流:
- 在
selectionchange回调中,用requestIdleCallback或setTimeout(..., 0)延迟读取,合并连续触发 - 若用于协作光标(如多人编辑显示他人光标),将
cursorPosition作为响应式字段,变更时广播给其他客户端;本地 setter 中恢复光标前先比对是否已过期(如时间戳或版本号) - 在 Vue 中可配合
watch(() => instance.cursorPosition, ...);React 中可用useEffect(() => { const unsub = editor.on('cursor:update', ...); return unsub; }, [])










