跳到主要内容
版本: v18.0.0

运行时架构

Relay 运行时是一个功能齐全的 GraphQL 客户端,它设计用于即使在低端移动设备上也能实现高性能,并且能够扩展到大型、复杂的应用程序。运行时 API 不打算在产品代码中直接使用,而是为了构建更高级的产品 API(例如 React/Relay)提供基础。该基础包括

  • 一个规范化的、内存中的对象图/缓存。
  • 一个优化的“写入”操作,用于使用查询/突变/订阅的结果更新缓存。
  • 一种机制,用于从缓存中读取数据并订阅更新,当这些结果因突变、订阅更新等而发生变化时。
  • 垃圾回收机制,用于在缓存中的条目不再被任何视图引用时将其清除。
  • 一个通用的机制,用于在将数据发布到缓存之前拦截数据,并合成新数据或将新数据和现有数据合并在一起(这在其他功能中允许创建各种分页方案)。
  • 具有乐观更新的突变,以及使用任意逻辑更新缓存的能力。
  • 支持网络/服务器支持的实时查询。
  • 用于启用订阅的核心原语。
  • 用于构建离线/持久化缓存的核心原语。

数据类型

  • DataID (类型): 记录的全局唯一或客户端生成的标识符,存储为字符串。
  • Record (类型): 表示具有标识、类型和字段的独特数据实体。请注意,实际的运行时表示对于系统来说是不透明的:对 Record 对象的所有访问(包括记录创建)都是通过 RelayModernRecord 模块进行的中介。这允许在单个位置更改表示本身(例如,使用 Map 或自定义类)。重要的是,其他代码不要假设 Record 始终是普通对象。
  • RecordSource (类型): 按其数据 ID 键控的记录集合,用于表示缓存及其更新。例如,存储的记录缓存是 RecordSource,查询/突变/订阅的结果被规范化为 RecordSource,这些 RecordSource 被发布到存储中。源还定义了用于异步加载记录的方法,以便(最终)支持离线用例。目前,该接口的唯一实现是 RelayInMemoryRecordSource;未来的实现可能会添加对从磁盘加载记录的支持。
  • Store (类型): RelayRuntime 实例的真相来源,以 RecordSource 的形式保存规范的记录集(尽管这不是必需的)。目前唯一实现的是 RelayModernStore
  • Network (类型): 提供从外部数据源获取查询数据和执行突变的方法。
  • Environment (类型): 表示将 StoreNetwork 结合在一起的封装环境,提供与两者交互的高级 API。这是 RelayRuntime 的主要公共 API。

用于处理查询及其结果的类型包括

  • Selector (类型): 选择器定义了遍历图的起点,用于定位子图,将 GraphQL 片段、变量和用于遍历应开始的根对象的 Data ID 结合在一起。直观地说,这“选择”了对象图的一部分。
  • Snapshot (类型): 在给定时间点执行 Selector 的(不可变)结果。这包括选择器本身、执行它的结果以及从其检索数据的 Data ID 列表(在确定这些结果何时可能发生变化时很有用)。

数据模型

Relay 运行时设计用于与 GraphQL 架构一起使用,GraphQL 架构描述对象图,其中对象具有类型、标识和一组具有值的字段。对象可以相互引用,这由字段的值是图中的一个或多个其他对象来表示。[1]. 为了与 JavaScript Object 区分,这些数据单元被称为 Record。Relay 将其内部缓存以及查询/突变/等结果表示为数据 ID记录的映射。数据 ID 是记录的唯一(相对于缓存)标识符 - 它可能是实际 id 字段的值,也可能是基于从最近具有 id 的对象的路径到记录(这种基于路径的 ID 被称为客户端 ID)。每个 Record 存储其数据 ID、类型和任何已获取的字段。多个记录一起存储为 RecordSource:数据 ID 到 Record 实例的映射。

例如,用户及其地址可能表示如下


// GraphQL Fragment
fragment on User {
id
name
address {
city
}
}

// Response
{
id: '842472',
name: 'Joe',
address: {
city: 'Seattle',
}
}

// Normalized Representation
RecordSource {
'842472': Record {
__id: '842472',
__typename: 'User', // the type is known statically from the fragment
id: '842472',
name: 'Joe',
address: {__ref: 'client:842472:address'}, // link to another record
},
'client:842472:address': Record {
// A client ID, derived from the path from parent & parent's ID
__id: 'client:842472:address',
__typename: 'Address',
city: 'Seattle',
}
}

[1]请注意,GraphQL 本身并没有强加这种约束,Relay 运行时也可以用于不符合这种约束的架构。例如,两个系统都可以用于查询单个非规范化表。但是,Relay 运行时提供的许多功能,例如缓存和规范化,在将数据表示为具有离散信息稳定标识的规范化图时效果最佳。

存储操作

Store 是应用程序数据的真相来源,并提供以下核心操作。

  • lookup(selector: Selector): Snapshot: 从存储中读取选择器的结果,返回给定存储中当前数据的值。

  • subscribe(snapshot: Snapshot, callback: (snapshot: Snapshot) => void): Disposable: 订阅对选择器结果的更改。当数据发布到存储中会导致快照的选择器结果发生变化时,就会调用回调。

  • publish(source: RecordSource): void: 使用新信息更新存储。所有对存储的更新都以这种形式表达,包括查询/突变/订阅的结果以及乐观突变更新。所有这些操作在内部都会创建一个新的 RecordSource 实例,并最终将其发布到存储中。请注意,publish() 不会立即更新任何 subscribe()-ers。在内部,存储会将新的 RecordSource 与其内部源进行比较,并在需要时进行更新。

    • 仅存在于已发布源中的记录将添加到存储中。
    • 同时存在于两者的记录将合并到一个新记录中(输入不变),并将结果添加到存储中。
    • 在已发布源中为 null 的记录将在存储中删除(设置为 null)。
    • 具有特殊哨兵值的记录将从存储中删除。这支持取消发布乐观创建的记录。
  • notify(): void: 调用任何 subscribe()-ers,其结果因介入的 publish()-es 而发生了变化。将 publish()notify() 分开,可以在执行任何下游更新逻辑(例如渲染)之前发布多个有效负载。

  • retain(selector: Selector): Disposable: 确保实现给定选择器所需的所有记录都保留在内存中。在返回的引用被释放之前,这些记录将不会有资格进行垃圾回收。

示例数据流:获取查询数据


┌───────────────────────┐
Query
└───────────────────────┘


┌ ─ ─ ─ ┐
fetch ◀────────────▶ Server
└ ─ ─ ─ ┘

┌─────┴───────┐
▼ ▼
┌──────────┐ ┌──────────┐
Query │ │ Response
└──────────┘ └──────────┘
│ │
└─────┬───────┘


normalize


┌───────────────────────┐
RecordSource
│ │
│┌──────┐┌──────┐┌─────┐│
││Record││Record││ ... ││
│└──────┘└──────┘└─────┘│
└───────────────────────┘

  1. 查询将从网络获取。
  2. 查询和响应一起遍历,将结果提取到 Record 对象中,这些对象被添加到一个新的 RecordSource 中。

这个新的 RecordSource 然后将被发布到存储中。


publish


┌───────────────────────────┐
Store
│ ┌───────────────────────┐ │
│ │ RecordSource │ │
│ │ │ │
│ │┌──────┐┌──────┐┌─────┐│ │
│ ││Record││Record││ ... ││ │ <--- records are updated
│ │└──────┘└──────┘└─────┘│ │
│ └───────────────────────┘ │
│ ┌───────────────────────┐ │
│ │ Subscriptions │ │
│ │ │ │
│ │┌──────┐┌──────┐┌─────┐│ │
│ ││ Sub. ││ Sub. ││ ... ││ │ <--- subscriptions do not fire yet
│ │└──────┘└──────┘└─────┘│ │
│ └───────────────────────┘ │
└───────────────────────────┘

发布结果会更新存储,但不会立即通知任何订阅者。这是通过调用 notify() 完成的……


notify


┌───────────────────────────┐
Store
│ ┌───────────────────────┐ │
│ │ RecordSource │ │
│ │ │ │
│ │┌──────┐┌──────┐┌─────┐│ │
│ ││Record││Record││ ... ││ │
│ │└──────┘└──────┘└─────┘│ │
│ └───────────────────────┘ │
│ ┌───────────────────────┐ │
│ │ Subscriptions │ │
│ │ │ │
│ │┌──────┐┌──────┐┌─────┐│ │
│ ││ Sub.││ Sub.││ ...││ │ <--- affected subscriptions fire
│ │└──────┘└──────┘└─────┘│ │
│ └───┼───────┼───────┼───┘ │
└─────┼───────┼───────┼─────┘
│ │ │
▼ │ │
callback │ │
▼ │
callback │

callback

…这会调用任何其结果发生变化的 subscribe()-ers 的回调。每个订阅都会按如下方式进行检查

  1. 首先,将自上次 notify() 以来发生更改的数据 ID 列表与订阅的最新 Snapshot 中列出的数据 ID 进行比较。如果没有重叠,则订阅的结果不可能发生变化(如果你想象图的视觉效果,则发生变化的部分图与被选择的图部分之间没有重叠)。在这种情况下,订阅将被忽略,否则继续处理。
  2. 其次,任何具有重叠数据 ID 的订阅都会被重新读取,并将新/旧结果进行比较。如果结果没有发生变化,则订阅将被忽略(如果记录的某个字段发生了变化,但与订阅的选择器无关,则可能发生这种情况),否则继续处理。
  3. 最后,数据实际发生变化的订阅将通过其回调进行通知。

示例数据流:读取和观察存储

产品主要通过 lookup()subscribe() 访问存储。Lookup 读取片段的初始结果,subscribe 观察该结果以进行任何更改。请注意,lookup() 的输出 - 一个 Snapshot - 是 subscribe() 的输入。这很重要,因为快照包含可用于优化订阅的重要信息 - 如果 subscribe() 仅接受 Selector,它将不得不重新读取结果以了解要订阅的内容,这效率较低。

因此,典型的数据流如下 - 请注意,此流程由更高级别的 API(如 React/Relay)自动管理。首先,组件将针对记录源(例如存储的规范源)查找选择器的结果。


┌───────────────────────┐ ┌──────────────┐
RecordSource │ │ │
│ │ │ │
│┌──────┐┌──────┐┌─────┐│ │ Selector
││Record││Record││ ... ││ │ │
│└──────┘└──────┘└─────┘│ │ │
└───────────────────────┘ └──────────────┘
│ │
│ │
└──────────────┬────────────┘

│ lookup
(read)


┌─────────────┐
│ │
Snapshot
│ │
└─────────────┘

│ render, etc



接下来,它将使用此快照 subscribe(),以便在发生任何更改时收到通知 - 请参阅上图以了解 publish()notify()


此页面是否有用?

通过以下方式帮助我们改进网站 回答几个快速问题.