「feat-CRDT」Yjs Doc - 1

583 阅读3分钟

「这是我参与2022首次更文挑战的第 16 天,活动详情查看:2022首次更文挑战」。


本文主旨:向大家大致解释了Yjs内部的工作方式。同时我们有一个完整的会议记录,里面有可用的Yjs代码库演示操作: youtu.be/0l5XgnQ6rB4

Yjs 中的CRDT算法在2016年的 YATA论文 中有描述。从算法的角度来看它是如何工作的,该论文是一个合理的起点。不过在Yjs中有一些小的改进并没有在论文中描述。其中最值得注意的一个:items 具有一个 originRightorigin 属性,当对同一个字符进行并发插入时,这可以提高性能。

核心

Yjs的核心是一个CRDT list。所有内容都被压入到一个list中,然后反复执行CRDT解析算法:

  • 数组很好理解 —— 它们是任意items的列表。
  • 文本是一个字符列表,可以选择用格式化标记和嵌入来支持富文本。几个字符可以包装在一个单独的链表 Item 中(这也称为CRDT的复合表示)。更多信息请参见 这篇博客文章
  • Mapentry 的列表。每个键的最后一个插入 entry 会被执行使用,而所有其他副本被标记为已删除。

每个客户端在第一次插入时会被分配一个唯一的 clientID。这是一个随机的53位整数(53位是因为它符合javascript的安全整数范围)。

Item 列表

Yjs list 中的每个 item 都是由两个对象组成的:

  • Item(src/structs/Item.js): 它用于将该项目与其他相邻的项目联系起来。
  • AbstractType层次结构中的一个对象(src/types/AbstractType.js 子类 —— 例如 YText)。这将实际内容存储在 Yjs document 中。

item和类型对象是一对一的关系。item的 content 字段引用 AbstractType 对象,而AbstractType 对象的 _item 字段引用该 item。

插入到 Yjs document 中的所有内容项都有一个唯一的ID,由ID(clientID,clock)对(也称为Lamport Timestamp)组成。客户端插入的第一个字符或item开始计,clock 从0开始计数。这类似于 automerge 的操作id。但是要注意,clock 只会随着插入而增加。删除的处理方式非常不同(见下文)。

如果一组字符被插入到一个文档中(例如 “abc”),每个字符加入,clock都会+1(例如这里结果:3次)。但是Yjs只会将一个Item添加到列表中。这对核心的CRDT算法没有影响,但这种优化极大地减少了正常文本编辑期间创建的js对象的数量。

这种优化只适用于字符共享相同clientID的情况,它们是按照顺序插入,并且所有字符被删除或所有字符未被删除。如果运行过程中因任何原因而中断(例如运行中其中一个字符被删除),该 item 将被分割。

当一个 item 被创建时,它存储了对前一个和后一个项目的ID的引用(head←me→next)。它们分别存储在 itemorigin & originRight 字段。当两个人同时在文档中的同一位置插入时,就会用到这些ID。虽然在实践中相当不常见,但Yjs需要确保 list items 在所有操作者上总是解析为相同的顺序。实际的逻辑相对简单 —— 只有几十行代码,位于 Item#integrate() 中。YATA论文中有更多关于该算法的细节。