文档核心概念容器 (Container)

容器

容器是 Loro 用于组织和构建协作数据的基础组件。它们提供带类型的数据结构,在发生并发编辑时会自动合并。

容器类型

Loro 提供多种容器类型,针对不同场景进行了优化:

  • LoroMap:带有最后写入生效(Last-Write-Wins)语义的键值对
  • LoroList:有序序列,可合并并发插入
  • LoroText:字符级合并并支持富文本
  • LoroTree:支持移动操作的层级树结构
  • LoroMovableList:可重新排序的列表
  • LoroCounter:支持增减操作的数值容器

容器状态:Attached 与 Detached

在 Loro 中,容器存在两种状态,不同状态会影响行为和标识。

Detached 容器

通过构造函数直接创建的容器处于 detached 状态:

// 这些容器都处于 detached 状态
const  = new ();
const  = new ();
const  = new ();

Detached 容器的特点:

  • 尚未加入任何文档
  • 拥有默认的占位 ContainerID
  • 可作为模板或临时数据结构使用
  • 插入文档后才会获得正式的 ContainerID

Attached 容器

当容器成为文档层级的一部分时,即进入 attached 状态:

const  = new ();
 
// 根容器创建后立即是 attached 状态
const  = .("myMap");
const  = .("myText");
 
// 子容器:返回值是 attached 状态
const  = new ();
const  = .("child", );
// 注意:detachedChild 仍然是 detached
// attachedChild 拥有正确的 ContainerID

Attached 容器的特点:

  • 隶属于某个具体文档
  • 拥有唯一标识它的 ContainerID
  • 变更会记录在文档历史中
  • 可以在节点之间同步

容器 ID

每个 attached 容器都会在分布式系统中拥有唯一的 ContainerID。ID 的生成取决于容器类型:

  • 根容器:依据名称生成(例如 doc.getMap("myMap") 中的 “myMap”)
  • 子容器:基于创建它的操作(OpID)生成

这种确定性的 ID 生成策略确保:

  • 所有节点都能识别出同一个容器
  • 容器 ID 不是随机的,而是由上下文决定
  • Detached 容器在插入前无法获得最终 ID

操作容器

创建根容器

根容器通过文档 API 创建,创建后立即处于 attached 状态:

const  = new ();
 
// 这些方法用于创建或获取根容器
const  = .("settings");
const  = .("content");
const  = .("items");
const  = .("hierarchy");

嵌套容器

容器可以相互嵌套,构建复杂的数据结构:

const  = new ();
const  = .("root");
 
// 方法 1:使用 setContainer(返回 attached 容器)
const  = .("description", new ());
 
// 方法 2:对于列表使用 insertContainer
const  = .("items");
const  = .(0, new ());

容器覆盖

在并行初始化子容器时,Loro 会发生覆盖而非自动合并。例如:

const : string = "hello";
const  = new ();
const  = .("map");
 
// 并行初始化子容器
const  = .();
const  = .("map").("text", new ());
.(0, "A");
const  = .("map").("text", new ());
.(0, "B");
 
.(.({ : "update" }));
// 结果:要么 { "meta": { "text": "A" } },要么 { "meta": { "text": "B" } }

如果没有保存编辑历史,这种行为会带来严重的数据丢失风险。即便保留了完整历史能够回滚恢复,操作流程也会非常复杂。

当容器承载大量数据,或是文档的主存储区域时,覆盖会隐藏或丢失关键信息。因此需要在容器初始化阶段采取谨慎、系统化的策略,避免出现问题。

最佳实践

  1. 如果可能发生并发初始化,尽量在根层级初始化容器,而不是作为嵌套容器
  2. 当使用 Map 容器时:
    • 尽量在创建 Map 容器时一次性初始化所有子容器
    • 避免在 Map 中并发创建同名子容器,以防止覆盖

出现覆盖是因为并行创建子容器会生成不同的 ContainerID,导致无法自动合并内容。

相关概念