我最喜欢的错误:无效的代理对

2026-05-16 1 阅读 meysamazad
如果你从事构建在计算机上运行的东西足够长的时间,我想你最终会获得一个最喜欢的错误故事。这是一个关于我的短篇故事。我还构建了一个交互式工具,您可以在其中探索支撑此错误核心的概念。错误:两个表情符号输入,没有一个离开我正在努力将旧版编辑器迁移到与我的团队更具协作性的体验。提示点击顶部(本身是 ProseMirror 的包装器),Yjs 在下面处理 CRDT 魔法以实现实时同步。效果很好!大多。在我们的 alpha/早期发布日,当它仍然主要是内部和/或早期推出用户时,有时编辑器会停止保存您的内容。默默。您继续输入,一切看起来都很好,但您的编辑停止同步到 Yjs 文档。下次打开该页面时,您在故障点之后编写的所有内容都消失了。这是非常可怕的,非常罕见,几乎不可能诊断,因为我们永远无法重现它。我们真的尽力了!我早期的怀疑主要围绕着不稳定的 wifi 连接和不稳定的 websocket 行为,但无论进行多少节流或打开和关闭我的 wifi 似乎都无法重现该问题。在我的记忆中,在这些场景中,这种经历令人惊讶地有弹性。感觉就像是随机发生的,从来没有人在看。控制台中没有发现明显的错误,没有堆栈跟踪,没有崩溃。只是...“嘿,我想我的更改没有保存。”然后有一天我们的产品经理解决了这个问题。这不是一件小事。他比任何人都经历过更多的事情(可能是因为他最擅长测试我们的产品)并且一直在有条不紊地缩小范围。 “我觉得我快要疯了,但我认为当我一起输入特定字符,然后返回并在它们之间插入一个字符时……”他一直在每周的项目状态电子邮件中使用 ? 和 ? 来传达总体健康状况。绿色表示正常,红色表示有风险。每周,他使用的模板都已经存在两个角色,他会简单地删除不需要的角色(我很高兴地说,通常是红色角色!)。这次他复制了绿色圆圈,并在某个时候将其粘贴到红色圆圈的前面,或者反之亦然。该特定操作(将一个多字节表情符号插入到另一个相邻的多字节表情符号)会触发底层 CRDT 库中的拼接,从而将代理对从中间分开。我记得当他向我和我的一位直接下属展示这个时,我正在通话中,他一直在为协作编辑过渡而努力。我一定是有点太兴奋了——我为深奥的虫子而生——“我觉得你被这个激励了,”他说。他没有错。更有趣的是,并不是每个表情符号都会触发它。只有 U+FFFF 以上的才需要代理对。并不是所有的编辑都会导致问题——只有那些在完全错误的字节偏移处导致拼接的编辑才会导致问题。在我们知道发生了什么之前,调试是一件很疯狂的事情。代码单元、代码点和字素簇 那么到底发生了什么?最后一段中的“U+FFFF 以上”是什么意思?什么字节偏移?为了理解这个错误,我们需要引入三个词汇:代码单元→代码点→字形簇代码单元是JavaScript用来在内部存储字符串的原始16位值(UTF-16)。这就是 .length 的意义。这也是 .slice() 和 .charCodeAt() 的操作对象。默认情况下,JavaScript 在代码单元级别运行 代码点是 Unicode 实际上定义为单个字符的内容。像 U+1F920 (?) 这样的代码点在 Unicode 看来是一个字符,但它太大了,无法容纳在单个 16 位代码单元中。因此 UTF-16 将其分为两个称为代理项对的代码单元:高代理项和低代理项。简单的 ASCII 字符和许多常见符号都适合一个代码单元,因此它们之间的区别并不重要。不过表情符号吗?几乎总是两个。字素簇是人类所感知的“一个字符”。女宇航员 ?‍? 看起来像一个角色,但实际上是三个代码点粘合在一起:?(女性)+ 零宽度连接器 + ?(火箭)。五个代码单元,三个代码点,一个字素。看似简单的?‍?‍?‍?(家庭:男人,男人,女孩,女孩)表情符号是令人印象深刻的十一!神秘的 ☃ 是 1。以下是这些数字的分歧: 代码单位 代码点 字形 A 1 1 1 ? 2 1 1 ?‍? 5 3 1 ?‍?‍?‍? 11 7 1 我将暂停,再次插入我在顶部提到的交互式代理浏览器。您可以输入任何表情符号并亲自查看详细信息! .slice() 如何破坏牛仔 ? 是一个存储为两个代码单元(代理对)的代码点。如果你在它们之间切片: "?".slice(0, 1); // → '\uD83E' (单独的高代理) "?".slice(1, 2); // → '\uDD20'(单独的低代理)这些片段不是有效字符。他们是半对,没有伴侣。它们自行渲染为替换字符 (�) 或被默默吞没。但r