Skip to main content

run-example-ab

好的,我用 ASCII 字符画一个时间轴图,模拟两个用户并发插入后,Yjs 内部的链表变化过程。


初始文档:"ab"

两个字符的 ID 分别为:

  • a(1, 1) (属于用户 A)
  • b(1, 2) (属于用户 A,初始全由 A 创建)

链表结构(双向,箭头表示指针):

null ← (1,1) : 'a' → (1,2) : 'b' → null
↑ ↑
prev next

并发操作

  • 用户 A 想在 a 后、b 前插入 'x',生成新 ID (1, 3)
  • 用户 B 想在 a 后、b 前插入 'y',生成新 ID (2, 1)

两人几乎同时执行,各自本地修改,尚未收到对方更改。


阶段 1:A 本地修改后

A 的链表:

null ← (1,1):a → (1,3):x → (1,2):b → null

A 看到的文档:a x b

阶段 1:B 本地修改后

B 的链表:

null ← (1,1):a → (2,1):y → (1,2):b → null

B 看到的文档:a y b


阶段 2:交换更新(A 收到 B 的操作)

B 发送给 A 的内容:插入(ID=(2,1):'y', 左邻居=(1,1), 右邻居=(1,2))

A 在自己链表中找到 (1,1)(1,2),把 (2,1) 插入中间。

插入后 A 的链表(按 ID 插入顺序,注意指针维护):

null ← (1,1):a → (1,3):x → (2,1):y → (1,2):b → null

顺序依赖:
从左到右遍历(按节点到达顺序,实际 Yjs 会根据 ID 中 clientID 的某种约定排序渲染,但最终两人会一致)


阶段 3:B 收到 A 的操作(对称)

B 收到 A 的 插入(ID=(1,3):'x', 左=(1,1), 右=(1,2))

B 在自己的链表中找到 (1,1)(1,2),插入 (1,3)

B 的链表变化:

原来:(1,1):a(2,1):y(1,2):b
插入后:(1,1):a(1,3):x(2,1):y(1,2):b


最终两人链表完全一致

位置: ① ② ③ ④
ID: (1,1) (1,3) (2,1) (1,2)
字符: a x y b

显示时按 ID 的 clientID 优先(也可按时间戳)排序,比如约定 clientID 小的先显示。
这里 A 的 clientID=1,B 的=2,所以顺序是 a (1,1)x (1,3)y (2,1)b (1,2) → 文档最终为 a x y b

两人都看到 a x y b,完全一致。


文字版时间轴(从上往下读)

时间 →
─────────────────────────────────────────────
初始: a (1,1) <--> b (1,2)

A 本地插入 x: a (1,1) <--> x (1,3) <--> b (1,2)
B 本地插入 y: a (1,1) <--> y (2,1) <--> b (1,2)

⬇ 交换操作(网络传递) ⬇

A 收到 y 插入: a → x → y → b
B 收到 x 插入: a → x → y → b

最终一致: a (1,1) → x (1,3) → y (2,1) → b (1,2)
─────────────────────────────────────────────
显示文档: a x y b

这样画是否帮你理解了“基于链表 + 全局唯一 ID + 前后锚点”的方式,如何自然合并并发操作而不需要转换?
如果还需要画删除后墓碑标记的图,我可以继续。