第二章:Document 模块与 DOM 树详解

第二章:Document 模块与 DOM 树详解

2.1 引言

flutter-quill 富文本编辑器中,Document 模块是整个框架的核心,负责管理文档的结构、内容和状态。Document 不仅是一个数据模型,还通过树状结构(类似浏览器的 DOM 树)实现了高效的内容操作和渲染支持。本章将深入分析 Document 类的底层原理,详细解析其源码实现,全面覆盖 nodes 目录下的所有文件类,探讨它们与 Document 的关系,并通过图示和案例详细说明 DOM 树的组成和原理。


2.2 Document 类的底层原理

2.2.1 概述

Document 类定义在 document.dart 文件中,是 flutter-quill 中富文本内容的核心表示。它通过树状结构组织内容,结合 Delta 格式实现内容的动态更新和序列化。以下是 Document 类的核心功能:

  • 内容存储:以节点树的形式存储富文本内容。
  • 操作接口:提供插入、删除、格式化等编辑操作。
  • 历史管理:支持撤销和重做功能。
  • 序列化 :将文档内容序列化为 Delta 格式,便于存储和传输。

2.2.2 源码分析:核心属性与初始化

以下是 Document 类的核心定义和初始化逻辑:

scss 复制代码
class Document {
  final Container _root;
  final History _history;

  Document({Delta? delta})
      : _root = Container()..insert('\n'),
        _history = History() {
    if (delta != null) {
      compose(delta, ChangeSource.local);
    }
  }

  Document.fromDelta(Delta delta) : this(delta: delta);

  Document.fromJson(List<dynamic> json) : this(delta: Delta.fromJson(json));
}
核心属性解析
  • _root

    • 类型Container
    • 作用 :文档的根节点,是整个节点树的起点。Container 是一种特殊的节点类型,可以包含多个子节点(如 LineLeaf)。
    • 初始化逻辑 :在构造函数中,_root 被初始化为一个空的 Container,并插入一个换行符 \n,确保文档至少包含一行。这是为了避免空文档导致的异常,同时为后续插入操作提供一个起点。
    • 底层实现_root 是一个链表结构,通过 nextprevious 属性连接子节点。每个子节点都有一个 parent 属性指向其父节点,从而形成树状结构。
  • _history

    • 类型History
    • 作用:管理文档的变更历史,支持撤销(undo)和重做(redo)功能。
    • 初始化逻辑_history 在构造函数中被初始化为一个空的 History 实例。它维护了一个操作栈,每次文档发生变化时(如通过 compose 方法),都会将变更记录到栈中。
    • 底层实现History 使用两个栈(undoStackredoStack)分别存储撤销和重做的操作。每个操作以 DocChange 的形式存储,包含变更的 Delta 和来源信息。
初始化逻辑
  • 默认构造函数Document({Delta? delta}) 创建一个空文档,并插入一个换行符。如果提供了 delta 参数,则通过 compose 方法将初始内容应用到文档中。
  • Delta 构造Document.fromDelta(Delta delta) 是工厂构造函数,调用默认构造函数并传入 delta
  • 从 JSON 构造Document.fromJson(List<dynamic> json) 将 JSON 数据反序列化为 Delta,然后构造文档。

示例:初始化文档

ini 复制代码
final doc = Document();
print(doc.toDelta()); // 输出: [{insert: "\n"}]

final delta = Delta()..insert('Hello');
final doc2 = Document.fromDelta(delta);
print(doc2.toDelta()); // 输出: [{insert: "Hello\n"}]

2.2.3 底层实现:节点树与 Delta 的交互

Document 的底层核心在于节点树(DOM 树)与 Delta 格式的协同工作。以下是交互的详细流程:

  1. 节点树的构建

    • 节点树以 _root 为起点,包含多层嵌套的节点。每个节点要么是 Container(如 Line),要么是 Leaf(如 TextNodeEmbedNode)。
    • 插入操作会动态创建节点并添加到树中。例如,插入文本 "Hello" 会创建一个 Line 节点和一个 TextNode
  2. 通过 Delta 更新节点树

    • Delta 是一种操作序列格式,包含 insertdeleteretain 操作。Document 通过 compose 方法将 Delta 应用到节点树。
    • 例如,Delta()..insert('Hello') 会创建一个新的 TextNode,并将其添加到适当的 Line 节点中。
  3. 从节点树生成 Delta

    • toDelta 方法遍历节点树,将每个节点的内容和样式序列化为 Delta 格式。

2.2.4 重要方法的底层实现逻辑

compose(Delta delta, ChangeSource source)
  • 功能 :将 Delta 操作应用到文档的节点树上。
  • 源码
scss 复制代码
void compose(Delta delta, ChangeSource source) {
  delta.trim();
  final change = _root.compose(delta, source);
  if (change.isNotEmpty) {
    _history.handleDocChange(change);
    notifyListeners(change, source);
  }
}
  • 实现逻辑

    1. 修剪 Deltadelta.trim() 移除末尾无意义的 retain 操作,优化性能。

    2. 应用变更 :调用 _root.compose(delta, source),将 Delta 传播到节点树。Container.compose 方法会:

      • 遍历 delta 中的每个操作(Operation)。
      • 根据操作类型(insertdeleteretain),更新或创建节点。
      • 例如,insert 操作会创建一个新的 TextNodeEmbedNode
    3. 记录历史 :如果生成了有效的 DocChange(变更记录),则通过 _history.handleDocChange 记录到历史栈。

    4. 通知监听器 :调用 notifyListeners 通知外部(如 UI 层)文档已更新。

  • 示例

    • 初始文档:"Hello"

    • Delta[{retain: 5}, {insert: " World"}]

    • 执行过程:

      1. retain 5 定位到 "Hello" 末尾。
      2. insert " World" 创建 TextNode(" World"),插入到节点树。
      3. 结果:"Hello World"
toDelta()
  • 功能 :将节点树序列化为 Delta 格式。
  • 源码
scss 复制代码
Delta toDelta() => _root.toDelta();
  • 实现逻辑

    1. _root 开始,调用 Container.toDelta()

    2. Container.toDelta() 递归遍历所有子节点:

      • 对于 Line 节点,收集其 Leaf 节点的 Delta 表示。
      • 对于 TextNode,返回 {insert: text, attributes: {...}}
      • 对于 EmbedNode,返回 {insert: {type: data}}
    3. 合并所有节点的 Delta,生成完整的文档快照。

  • 示例

    • 文档结构:

      scss 复制代码
      Container (root)
      └── Line
          ├── TextNode("Hello", {bold: true})
          └── TextNode(" World")
    • 输出:[{insert: "Hello", attributes: {bold: true}}, {insert: " World"}]

insert(int index, dynamic data)
  • 功能:在指定位置插入内容。
  • 源码
scss 复制代码
void insert(int index, dynamic data, {Attribute? attribute}) {
  final delta = Delta()
    ..retain(index)
    ..insert(data, attribute?.toJson());
  compose(delta, ChangeSource.local);
}
  • 实现逻辑

    1. 构造 Delta

      • retain(index):定位到插入位置。
      • insert(data, attributes):插入内容和样式。
    2. 调用 compose 方法,将 Delta 应用到节点树。

    3. 在节点树中,Container.compose 会:

      • 找到插入位置对应的 LineLeaf
      • 创建新的 TextNodeEmbedNode,插入到适当位置。
  • 示例

    • 初始文档:"Hello"
    • 调用:insert(5, " World")
    • 生成 Delta[{retain: 5}, {insert: " World"}]
    • 结果:"Hello World"
delete(int index, int length)
  • 功能:从指定位置删除指定长度的内容。
  • 源码
scss 复制代码
void delete(int index, int len) {
  final delta = Delta()
    ..retain(index)
    ..delete(len);
  compose(delta, ChangeSource.local);
}
  • 实现逻辑

    1. 构造 Delta

      • retain(index):定位到删除起点。
      • delete(len):删除指定长度。
    2. 调用 compose 方法。

    3. 在节点树中,Container.compose 会:

      • 找到删除范围对应的 Leaf 节点。
      • 移除或调整节点内容。
  • 示例

    • 初始文档:"Hello World"
    • 调用:delete(5, 6)
    • 生成 Delta[{retain: 5}, {delete: 6}]
    • 结果:"Hello"
format(int index, int length, Attribute attribute)
  • 功能:为指定范围应用样式。
  • 源码
scss 复制代码
void format(int index, int len, Attribute attribute) {
  final delta = Delta()
    ..retain(index)
    ..retain(len, attribute.toJson());
  compose(delta, ChangeSource.local);
}
  • 实现逻辑

    1. 构造 Delta

      • retain(index):定位到格式化起点。
      • retain(len, attributes):为指定范围应用样式。
    2. 调用 compose 方法。

    3. Container.compose 会更新对应 Leaf 节点的样式。

  • 示例

    • 初始文档:"Hello World"
    • 调用:format(0, 5, Attribute.bold)
    • 生成 Delta[{retain: 5, attributes: {bold: true}}]
    • 结果:"Hello" 变为粗体。

2.2.5 性能优化与设计考量

  • 链表结构:节点树使用链表而非数组,便于动态插入和删除操作。
  • 增量更新 :通过 Delta 实现增量更新,避免全量重建节点树。
  • 历史管理History 的栈式设计支持高效的撤销/重做。
  • 惰性计算 :如 length 属性通过递归计算,避免频繁更新。

2.3 nodes 目录下的文件类详解及其与 Document 的关系

nodes 目录包含文档节点系统的核心实现,共有以下文件:node.dartline.dartleaf.dartembed.dart。这些类共同构成了 Document 的节点树。

2.3.1 node.dart

  • 作用:定义节点系统的基类和通用接口。

  • 核心类

    • Node

      • 定义:抽象基类,定义了节点的通用方法和属性。

      • 属性

        • parent:父节点,指向 Container
        • next:下一个兄弟节点。
        • previous:上一个兄弟节点。
      • 方法

        • insert(int index, dynamic data):插入内容。
        • delete(int index, int length):删除内容。
        • toDelta():序列化为 Delta
      • Document 的关系Node 是所有节点的基类,Document_root 及其子节点都是 Node 的实例。

    • Container

      • 定义 :继承自 Node,表示可包含子节点的容器。

      • 属性

        • children:子节点列表。
      • 方法

        • appendChild(Node child):添加子节点。
        • removeChild(Node child):移除子节点。
        • compose(Delta delta, ChangeSource source):应用 Delta 操作。
      • Document 的关系_rootContainer 的实例,作为节点树的根节点,负责管理所有子节点。

    • Leaf

      • 定义 :抽象类,继承自 Node,表示叶子节点。

      • 方法

        • applyAttribute(Attribute attr):应用样式。
      • Document 的关系Leaf 是文档内容的最小单元,直接存储文本或嵌入对象。

示例:构建节点关系

ini 复制代码
final container = Container();
final leaf = TextNode('Text');
container.appendChild(leaf);
assert(leaf.parent == container);

2.3.2 line.dart

  • 作用 :定义 Line 类,表示文档中的一行。

  • 实现

    • 继承自 Container,管理该行内的 Leaf 节点。
    • 支持行级样式(如对齐、缩进)。
  • 核心方法

    • applyAttribute(Attribute attr)

      • 功能:应用行级样式。
      • 实现:将样式存储在 Line 节点的 attributes 中,并通知子节点更新。
    • toDelta()

      • 功能:序列化该行的内容和样式。
      • 实现:遍历所有 Leaf 节点,收集它们的 Delta 表示。
  • Document 的关系

    • Line_root 的直接子节点,表示文档的一行内容。
    • 插入换行符时,Document 会创建一个新的 Line 节点。

示例:设置行样式

scss 复制代码
final line = Line();
line.insert(0, 'Centered Text');
line.applyAttribute(Attribute.center);
print(line.toDelta());
// 输出: [{insert: "Centered Text\n", attributes: {align: "center"}}]

2.3.3 leaf.dart

  • 作用 :定义 Leaf 类的具体实现,作为内容的最小单元。

  • 子类

    • TextNode

      • 功能:存储文本内容及其样式。

      • 属性

        • value:文本内容。
        • attributes:样式属性(如粗体、下划线)。
      • 方法

        • applyAttribute(Attribute attr):应用样式。
        • toDelta():序列化为 Delta
    • EmbedNode

      • 功能 :存储嵌入对象(详见 embed.dart)。
  • Document 的关系

    • Leaf 节点是 Line 的子节点,直接承载 Document 的内容。
    • 例如,insert 操作会在适当位置创建 TextNode

示例:文本样式

ini 复制代码
final textNode = TextNode('Bold Text');
textNode.applyAttribute(Attribute.bold);
print(textNode.toDelta());
// 输出: [{insert: "Bold Text", attributes: {bold: true}}]

2.3.4 embed.dart

  • 作用 :定义 EmbedNode 类,用于处理嵌入内容(如图片、视频)。

  • 实现

    • 继承自 Leaf,通过 Embeddable 接口支持扩展。

    • 属性

      • type:嵌入类型(如 "image")。
      • data:嵌入数据(如图片 URL)。
    • 方法

      • toDelta():序列化为 Delta
  • Document 的关系

    • EmbedNodeLeaf 的子类,与 TextNode 并列存储在 Line 中。
    • 插入嵌入对象时,Document 会创建 EmbedNode

示例:插入图片

ini 复制代码
final imageEmbed = EmbedNode('image', {'source': 'https://example.com/image.jpg'});
print(imageEmbed.toDelta());
// 输出: [{insert: {"image": "https://example.com/image.jpg"}}]

2.4 DOM 树的概念与组成

2.4.1 什么是 DOM 树?

DOM 树(Document Object Model Tree)是一种树状数据结构,用于表示文档的层次结构。在 flutter-quill 中,Document 的节点树就是一种 DOM 树,类似于浏览器中的 DOM,但专门为富文本编辑设计。DOM 树的核心特点是:

  • 层次结构:节点以父子关系组织,根节点是整个文档的起点。
  • 动态性:支持动态插入、删除和修改节点。
  • 内容表示:叶子节点存储实际内容(如文本或嵌入对象),容器节点定义结构(如行)。

2.4.2 DOM 树的组成

flutter-quill 中,DOM 树由以下节点类型组成:

  • 根节点 (Container)

    • 表示整个文档,通常是一个空的 Container,包含所有子节点。
    • Document_root 属性管理。
  • 行节点 (Line)

    • 表示文档中的一行,继承自 Container,包含多个 Leaf 节点。
    • 支持行级样式,如对齐、缩进。
  • 叶子节点 (Leaf)

    • TextNode:存储文本内容及其样式。
    • EmbedNode:存储嵌入对象,如图片或视频。

2.4.3 DOM 树图示

以下是一个示例文档的 DOM 树结构:

scss 复制代码
Document
└── Container (root)
    ├── Line
    │   ├── TextNode("Hello", attributes: {bold: true})
    │   └── TextNode(" World")
    ├── Line
    │   └── EmbedNode("image", data: {"source": "https://example.com/image.jpg"})
    └── Line
        └── TextNode("End")
  • 解释

    • 根节点Container (root)_root,包含三个 Line 节点。
    • 第一行 :包含两个 TextNode,表示带粗体样式的 "Hello" 和普通文本 " World"。
    • 第二行 :包含一个 EmbedNode,表示嵌入的图片。
    • 第三行 :包含一个 TextNode,表示文本 "End"。

2.4.4 DOM 树的动态变化

DOM 树是动态的,可以通过编辑操作实时更新。以下是几个常见操作及其对 DOM 树的影响:

  • 插入文本

    • 示例:insert(5, " New")

    • 初始 DOM 树:

      scss 复制代码
      Container (root)
      └── Line
          ├── TextNode("Hello")
          └── TextNode(" World")
    • 操作后:

      scss 复制代码
      Container (root)
      └── Line
          ├── TextNode("Hello")
          ├── TextNode(" New")
          └── TextNode(" World")
  • 删除内容

    • 示例:delete(5, 6)

    • 初始 DOM 树:

      scss 复制代码
      Container (root)
      └── Line
          ├── TextNode("Hello")
          └── TextNode(" World")
    • 操作后:

      scss 复制代码
      Container (root)
      └── Line
          └── TextNode("Hello")
  • 插入嵌入对象

    • 示例:insert(11, "", {"image": "url"})

    • 初始 DOM 树:

      scss 复制代码
      Container (root)
      └── Line
          ├── TextNode("Hello")
          └── TextNode(" World")
    • 操作后:

      scss 复制代码
      Container (root)
      ├── Line
      │   ├── TextNode("Hello")
      │   └── TextNode(" World")
      └── Line
          └── EmbedNode("image", data: {"source": "url"})

2.4.5 DOM 树与渲染的关系

DOM 树不仅是数据模型,也是渲染的基础。在 flutter-quill 中,QuillEditor 会遍历 DOM 树,将每个 LineLeaf 渲染为 Flutter 的 RichText 组件:

  • Line 节点对应一个段落。
  • TextNode 渲染为 TextSpan,应用其样式。
  • EmbedNode 渲染为自定义 Widget(如图片)。

2.5 综合案例

以下是一个综合案例,展示 Document 和节点系统的完整功能:

css 复制代码
// 创建文档
final doc = Document();

// 插入文本并应用样式
doc.insert(0, 'Hello World');
doc.format(0, 5, Attribute.bold);
doc.format(6, 5, Attribute.italic);

// 插入换行和嵌入对象
doc.insert(11, '\n');
doc.insert(12, '', {'image': 'https://example.com/pic.jpg'});

// 插入新行
doc.insert(13, '\nEnd');

// 检查 DOM 树
print(doc.toDelta());
// 输出: [{insert: "Hello", attributes: {bold: true}}, {insert: " World", attributes: {italic: true}}, {insert: "\n"}, {insert: {"image": "https://example.com/pic.jpg"}}, {insert: "\nEnd\n"}]

// 撤销操作
doc.undo();
print(doc.toDelta());
// 输出: [{insert: "Hello", attributes: {bold: true}}, {insert: " World", attributes: {italic: true}}, {insert: "\n"}, {insert: {"image": "https://example.com/pic.jpg"}}, {insert: "\n"}]

// 重做操作
doc.redo();
print(doc.toDelta());
// 输出: [{insert: "Hello", attributes: {bold: true}}, {insert: " World", attributes: {italic: true}}, {insert: "\n"}, {insert: {"image": "https://example.com/pic.jpg"}}, {insert: "\nEnd\n"}]
  • 步骤解析

    1. 创建文档Document() 初始化一个空文档,DOM 树为:

      scss 复制代码
      Container (root)
      └── Line
          └── TextNode("\n")
    2. 插入文本insert(0, 'Hello World') 更新 DOM 树为:

      scss 复制代码
      Container (root)
      └── Line
          └── TextNode("Hello World\n")
    3. 应用样式

      • format(0, 5, Attribute.bold) 将 "Hello" 设置为粗体。

      • format(6, 5, Attribute.italic) 将 "World" 设置为斜体。

      • DOM 树变为:

        php 复制代码
        Container (root)
        └── Line
            ├── TextNode("Hello", attributes: {bold: true})
            └── TextNode(" World\n", attributes: {italic: true})
    4. 插入嵌入对象

      • insert(11, '\n') 添加新行。

      • insert(12, '', {'image': 'url'}) 插入图片。

      • DOM 树变为:

        php 复制代码
        Container (root)
        ├── Line
        │   ├── TextNode("Hello", attributes: {bold: true})
        │   └── TextNode(" World", attributes: {italic: true})
        ├── Line
        │   └── EmbedNode("image", data: {"source": "url"})
        └── Line
            └── TextNode("\n")
    5. 插入新行insert(13, '\nEnd') 更新 DOM 树为:

      php 复制代码
      Container (root)
      ├── Line
      │   ├── TextNode("Hello", attributes: {bold: true})
      │   └── TextNode(" World", attributes: {italic: true})
      ├── Line
      │   └── EmbedNode("image", data: {"source": "url"})
      └── Line
          └── TextNode("End\n")
    6. 历史管理

      • undo() 撤销最后一次操作(插入 "End")。
      • redo() 重做该操作。

2.6 后续

关注后续的文章

相关推荐
LawrenceLan15 小时前
Flutter 零基础入门(九):构造函数、命名构造函数与 this 关键字
开发语言·flutter·dart
一豆羹16 小时前
macOS 环境下 ADB 无线调试连接失败、Protocol Fault 及端口占用的深度排查
flutter
行者9616 小时前
OpenHarmony上Flutter粒子效果组件的深度适配与实践
flutter·交互·harmonyos·鸿蒙
行者9619 小时前
Flutter与OpenHarmony深度集成:数据导出组件的实战优化与性能提升
flutter·harmonyos·鸿蒙
小雨下雨的雨19 小时前
Flutter 框架跨平台鸿蒙开发 —— Row & Column 布局之轴线控制艺术
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨19 小时前
Flutter 框架跨平台鸿蒙开发 —— Center 控件之完美居中之道
flutter·ui·华为·harmonyos·鸿蒙
小雨下雨的雨20 小时前
Flutter 框架跨平台鸿蒙开发 —— Icon 控件之图标交互美学
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨20 小时前
Flutter 框架跨平台鸿蒙开发 —— Placeholder 控件之布局雏形美学
flutter·ui·华为·harmonyos·鸿蒙系统
行者9621 小时前
OpenHarmony Flutter弹出菜单组件深度实践:从基础到高级的完整指南
flutter·harmonyos·鸿蒙
前端不太难21 小时前
Flutter / RN / iOS,在长期维护下的性能差异本质
flutter·ios