第二章: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 后续

关注后续的文章

相关推荐
阅文作家助手开发团队_山神13 小时前
第三章: Flutter-quill 数据格式Delta
flutter
程序员老刘13 小时前
20%的选择决定80%的成败
flutter·架构·客户端
肥肥呀呀呀1 天前
flutter 中Stack 使用clipBehavior: Clip.none, 超出的部分无法响应所有事件
flutter
SY.ZHOU1 天前
Flutter如何支持原生View
flutter
sg_knight1 天前
Flutter嵌入式开发实战 ——从树莓派到智能家居控制面板,打造工业级交互终端
android·前端·flutter·ios·智能家居·跨平台
张风捷特烈1 天前
每日一题 Flutter#4 | 说说组件 build 函数的作用
android·flutter·面试
小镇梦想家2 天前
鸿蒙NEXT-Flutter(2)
flutter
至善迎风2 天前
一键更新依赖全指南:Flutter、Node.js、Kotlin、Java、Go、Python 等主流语言全覆盖
java·flutter·node.js
椒盐煎蛋2 天前
新建的Flutter插件工程,无法索引andorid工程代码;无法索引io.flutter包下代码。
flutter