CodeJar 在中文输入法下 Backspace 误删大片文本的根因与修复实践

2026-02-03 35 浏览 0 评论

在为项目实现一个轻量级代码编辑器时,我选择了 CodeJar。它体积小、依赖少、基于 contenteditable 即可完成语法高亮,集成成本极低。然而在真实使用中遇到了一个非常棘手的问题:

在中文输入法环境下,按一次 Backspace,偶发性删除一大片文本。

这个问题最初看起来像是浏览器 bug,甚至一度怀疑是输入法行为异常。但经过定位后,发现问题来自 CodeJar 的高亮重渲染机制与浏览器 IME(输入法组合输入)事件的冲突。


现象复现

环境:

  • Chrome / Edge
  • 任意中文输入法
  • CodeJar 3.x

操作过程:

  1. 在编辑器中输入中文(处于组合输入状态)
  2. 输入法提交文字后立即按 Backspace
  3. 偶发性触发整段文本被删除

问题并非必现,而是随机出现,增加了排查难度。


代码结构分析

查看 CodeJar 源码后发现,新版已经不再使用 beforeinput 进行高亮同步,而是改为在 keyup 事件后触发延迟高亮:

on('keyup', event => {
  if (event.defaultPrevented) return;
  if (event.isComposing) return;
  if (prev !== toString()) debounceHighlight();
});

而高亮逻辑会执行:

const pos = save();
highlight(editor, pos);
restore(pos);

即:

  1. 保存当前 selection
  2. 重绘 DOM(插入语法高亮 HTML)
  3. 再恢复 selection

问题恰好出在 IME 组合输入刚结束时浏览器 selection 尚未稳定 ,而 CodeJar 在此时触发重绘并恢复 selection,会造成 selection 错位。下一次 Backspace 时,浏览器认为当前存在大范围选区,于是删除整片内容。


关键点

IME 组合输入期间不能触发高亮重渲染。

否则 selection 恢复的坐标会与浏览器内部状态不一致。


修复方案实现

CodeJar 内部已经提供统一事件注册封装:

const on = (type, fn) => editor.addEventListener(type, fn);

因此直接引入原生 compositionstart / compositionend 即可。

新增 IME 状态锁:

let isComposingIME = false;

监听输入法事件:

on('compositionstart', () => {
  isComposingIME = true;
});

on('compositionend', () => {
  isComposingIME = false;
  requestAnimationFrame(() => {
    const pos = save();
    doHighlight(editor, pos);
    restore(pos);
  });
});

并在 keyup 阶段阻止高亮触发:

on('keyup', event => {
  if (event.defaultPrevented) return;
  if (event.isComposing) return;
  if (isComposingIME) return;
  if (prev !== toString()) debounceHighlight();
});

最终逻辑变为:

  • IME 组合输入时不触发 DOM 重绘
  • 组合结束后在下一帧统一执行一次高亮
  • selection 全程保持稳定

修复结果

  • 中文输入正常
  • Backspace 精确删除单字符
  • 高亮行为保持一致
  • Undo / History 不受影响
  • 无需改动 CodeJar 内部核心结构

问题彻底消失。


结语

这类问题的本质是:

contenteditable + IME + DOM 重绘 + selection 恢复 四者同时存在时产生的状态竞争。

CodeJar 本身足够轻量,但在复杂输入环境下需要补足浏览器输入法事件处理。完成这一步后,它仍然是一个非常干净、可控、适合嵌入式场景的编辑器内核。


发布评论

发布评论前请先 登录

评论列表 0

暂无评论