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 + 前后锚点”的方式,如何自然合并并发操作而不需要转换?
如果还需要画删除后墓碑标记的图,我可以继续。