tmagic-editor/docs/runtime-api/data-source/dataSourceManager.md
roymondchen dfad691493 feat(data-source): 支持批量更新数据源
新增 dataSourceManager.batch(fn, options),fn 中的多次 setData 在结束后
统一更新一次页面:受影响节点只编译、替换一次,并按数据源分别触发 update-data。

- mergeChangeEvents 默认 false,每次变化各触发一次 update-data 并携带自己的
  changeEvent,不丢失中间变化;传 true 则每个数据源只触发一次
- 支持嵌套,仅支持同步函数;isBatching 暴露当前是否处于批量更新中
- 页面/页面片节点的替换改为批量 replaceChildNodes,避免每个节点全树搜索
- 数据源字段变更事件在 batch 内仍按每次 setData 触发
2026-10-30 16:47:56 +08:00

413 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DataSourceManager
`DataSourceManager` 是 `@tmagic/data-source` 的核心类,用于管理所有数据源的生命周期、数据编译和事件分发。
## 构造函数
```typescript
new DataSourceManager(options: DataSourceManagerOptions)
```
### DataSourceManagerOptions
| 参数 | 类型 | 说明 |
|------|------|------|
| `app` | `TMagicApp` | 应用实例 |
| `initialData` | `DataSourceManagerData` | 初始数据(可选,用于 SSR) |
| `useMock` | `boolean` | 是否使用 Mock 数据(可选) |
## 属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `app` | `TMagicApp` | 应用实例 |
| `dataSourceMap` | `Map<string, DataSource>` | 数据源映射表 |
| `data` | `DataSourceManagerData` | 所有数据源的数据 |
| `initialData` | `DataSourceManagerData` | 初始化数据 |
| `useMock` | `boolean` | 是否使用 Mock 数据 |
| `isMounted` | `boolean` | 页面是否已渲染完成(收到过 `mounted` 事件) |
## 静态方法
### register
- **参数:**
- `{string} type` 数据源类型
- `{typeof DataSource} dataSourceClass` 数据源类
- **返回:**
- `{void}`
- **详情:**
注册自定义数据源类型。
- **示例:**
```typescript
import { DataSourceManager } from '@tmagic/data-source';
class CustomDataSource extends DataSource {
// 自定义实现
}
DataSourceManager.register('custom', CustomDataSource);
```
### getDataSourceClass
- **参数:**
- `{string} type` 数据源类型
- **返回:**
- `{typeof DataSource | undefined}`
- **详情:**
获取已注册的数据源类。
### clearDataSourceClass
- **返回:**
- `{void}`
- **详情:**
清除所有已注册的数据源类(保留 `base` 和 `http`)。
### registerObservedData
- **参数:**
- `{ObservedDataClass} observedDataClass` 观察者数据类
- **返回:**
- `{void}`
- **详情:**
注册自定义的观察者数据类。
## 实例方法
### init
- **参数:**
- `{DataSource} ds` 单个数据源实例(必填)
- **返回:**
- `{Promise<void>}`
- **详情:**
初始化单个数据源。会按 `methods` 中 `timing === 'beforeInit'` 的方法依次执行,再调用 `ds.init()`,最后执行 `timing === 'afterInit'` 的方法。当 `ds.isInit` 为 `true`,或当前 `app.jsEngine` 命中 `ds.schema.disabledInitInJsEngine` 时直接跳过。
- **示例:**
```typescript
const ds = dataSourceManager.get('ds_1');
if (ds) {
await dataSourceManager.init(ds);
}
```
### mounted
- **参数:**
- `{DataSource} ds` 单个数据源实例(必填)
- **返回:**
- `{Promise<void>}`
- **详情:**
页面渲染完成后执行单个数据源的 `mounted`:先调用 `ds.mounted()`,再依次执行 `methods` 中 `timing === 'mounted'` 的方法。当 `ds.isMounted` 为 `true`,或当前 `app.jsEngine` 命中 `ds.schema.disabledInitInJsEngine` 时直接跳过。
一般不需要手动调用,runtime 在顶层组件渲染完成后触发 `mounted` 事件,由 manager 统一对所有数据源调用该方法。
- **示例:**
```typescript
// runtime 顶层组件渲染完成后
app.dataSourceManager?.emit('mounted');
```
### get
- **参数:**
- `{string} id` 数据源 ID
- **返回:**
- `{DataSource | undefined}`
- **详情:**
获取指定 ID 的数据源实例。
- **示例:**
```typescript
const ds = dataSourceManager.get('ds_1');
```
### addDataSource
- **参数:**
- `{DataSourceSchema} config` 数据源配置(可选)
- **返回:**
- `{DataSource | undefined}`
- **详情:**
添加新的数据源。当对应类型尚未注册(即 `dataSourceClassMap` 中无该类型)时,会将配置缓存到 `waitInitSchemaList`,并返回 `undefined`,待 `register` 时再补建实例。
- **示例:**
```typescript
const ds = dataSourceManager.addDataSource({
id: 'new_ds',
type: 'base',
fields: []
});
```
### removeDataSource
- **参数:**
- `{string} id` 数据源 ID
- **返回:**
- `{void}`
- **详情:**
移除指定的数据源。
### setData
- **参数:**
- `{DataSource} ds` 数据源实例
- `{ChangeEvent} changeEvent` 变化事件,形如 `{ updateData, path? }`
- **返回:**
- `{void}`
- **详情:**
将 `ds.data` 同步到 `this.data[ds.id]`,并以 `(ds.id, changeEvent)` 触发 `change` 事件。
### updateSchema
- **参数:**
- `{DataSourceSchema[]} schemas` 数据源配置数组
- **返回:**
- `{void}`
- **详情:**
同步更新数据源 DSL 配置:先按 `id` 移除已有数据源,再以 `cloneDeep` 重新 `addDataSource`,并对新建实例触发 `init`(异步执行,不会被该方法 `await`);若此时 `isMounted` 已为 `true`,`init` 完成后还会补充执行 `mounted`。一般在编辑器中修改配置后调用。
### compiledNode
- **参数:**
- `{MNode} node` 节点配置
- `{string} sourceId` 数据源 ID(可选)
- `{boolean} deep` 是否深度编译(可选)
- **返回:**
- `{MNode}`
- **详情:**
编译节点 DSL 中的数据源配置,将数据源引用替换为实际值。
- **示例:**
```typescript
const compiledNode = dataSourceManager.compiledNode({
id: 'text_1',
type: 'text',
text: '${ds_1.username}'
});
// compiledNode.text 会被替换为实际的用户名
```
### compliedConds
- **参数:**
- `{ { [NODE_CONDS_KEY]?: DisplayCond[]; [NODE_CONDS_RESULT_KEY]?: boolean; [NODE_DISABLE_DATA_SOURCE_KEY]?: boolean } } node` 带条件字段的结构(不要求是完整的 MNode)
- `{DataSourceManagerData} data` 数据上下文(可选,默认使用 `this.data`)
- **返回:**
- `{boolean}` 节点是否应该显示
- **详情:**
编译显示条件,返回节点是否应该显示。
- **示例:**
```typescript
const shouldShow = dataSourceManager.compliedConds(node);
```
### compliedIteratorItemConds
- **参数:**
- `{any} itemData` 迭代项数据
- `{ { [NODE_CONDS_KEY]?: DisplayCond[] } } node` 带条件字段的结构
- `{string[]} dataSourceField` 迭代数据在数据源中的字段路径,格式如 `['dsId', 'key1', 'key2']`(可选,默认 `[]`)
- **返回:**
- `{boolean}`
- **详情:**
编译迭代器项的显示条件。
### compliedIteratorItems
- **参数:**
- `{any} itemData` 迭代项数据
- `{MNode[]} nodes` 节点配置数组
- `{string[]} dataSourceField` 迭代数据在数据源中的字段路径(可选,默认 `[]`)
- **返回:**
- `{MNode[]}`
- **详情:**
编译迭代器项的节点配置。
### isAllDataSourceRegistered
- **返回:**
- `{boolean}`
- **详情:**
判断 DSL 中声明的所有数据源是否都已注册并实例化(即 `dataSourceMap.size === dsl.dataSources.length`,或 DSL 未声明数据源)。
::: tip `registered-all` 触发时机
`registered-all` 事件**仅**在 `addDataSource` 末尾、`isAllDataSourceRegistered()` 为真时触发。当 `dsl.dataSources` 为空数组时,构造函数不会进入任何一次 `addDataSource` 调用,因此**不会**触发该事件,但仍可能执行 `callDsInit()`。
:::
### onDataChange
- **参数:**
- `{string} id` 数据源 ID
- `{string} path` 数据路径
- `{(payload: any) => void} callback` 回调函数(具体入参取决于 `ObservedData` 实现,详见 [DataSource.onDataChange](./dataSource.md#ondatachange))
- `{ { immediate?: boolean } } options` 选项(可选)
- **返回:**
- `{void}`
- **详情:**
监听数据源数据变化。`options.immediate` 为 `true` 时,订阅后立即用当前值触发一次回调(具体语义取决于 `ObservedData` 的实现)。
- **示例:**
```typescript
dataSourceManager.onDataChange('ds_1', 'user.name', (newVal) => {
console.log('用户名变更:', newVal);
});
```
### offDataChange
- **参数:**
- `{string} id` 数据源 ID
- `{string} path` 数据路径
- `{(newVal: any) => void} callback` 回调函数
- **返回:**
- `{void}`
- **详情:**
取消监听数据源数据变化。
### batch
- **参数:**
- `{() => T} fn` 同步执行的函数
- `{BatchOptions} [options]` 批量更新选项,嵌套时以最外层为准
- `{boolean} [mergeChangeEvents=false]` 是否把同一数据源的多次变化合并成一次 `update-data`
- **返回:**
- `{T}` fn 的返回值
- **详情:**
批量更新。`fn` 中产生的所有数据变化,会在 `fn` 执行结束后(同步)统一更新一次页面:受影响的节点只编译、替换一次,并按数据源分别触发 `update-data`。
- 数据本身(`data`、`onDataChange`、`change` 事件)仍然实时生效,只是页面节点的更新被合并
- 支持嵌套,最外层 `batch` 结束时才统一更新
- 仅支持同步函数,`fn` 返回 Promise 时不会等待
`mergeChangeEvents` 默认为 `false`:每次 `setData` 各触发一次 `update-data`,各自携带自己的 `changeEvent`(`path`、`updateData`),与不使用 `batch` 时一致,监听 `update-data` / `replaced-node` 的业务不会漏掉中间变化。页面节点只编译一次,且渲染层会按节点对象去重(同一份已编译节点只替换一次),因此事件变多不会带来重复的替换开销。
传 `true` 时每个数据源只触发一次 `update-data`,`changeEvent` 是该数据源最后一次变化,中间变化会丢失;适合不关心每次变化、只想少收事件的场景。
```typescript
dataSourceManager.batch(() => {
ds1.setData(list, 'list');
ds1.setData(total, 'total');
ds2.setData(title, 'title');
});
// 每个数据源只触发一次 update-data,只带最后一次 changeEvent
dataSourceManager.batch(
() => {
ds1.setData(list, 'list');
ds1.setData(total, 'total');
},
{ mergeChangeEvents: true },
);
```
### isBatching
- **类型:** `{boolean}`
- **详情:**
当前是否处于 `batch` 中(只读属性)。
### destroy
- **返回:**
- `{void}`
- **详情:**
销毁数据源管理器,清理所有数据源。
## 事件
DataSourceManager 继承自 EventEmitter,支持以下事件:
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| `change` | 单个数据源数据变化 | `(dsId: string, changeEvent: ChangeEvent)` |
| `init` | 所有数据源初始化完成;现代分支携带 `(data, errors)`,旧 Promise.all 分支为 `(this.data)` | `(data, errors?)` |
| `mounted` | 由 runtime 在顶层组件渲染完成后触发,manager 收到后统一执行各数据源的 `mounted` | 无 |
| `registered-all` | 所有数据源注册完成 | 无 |
| `batch-change` | `batch` 结束时发出,携带期间所有的数据变化,`createDataSourceManager` 据此统一更新页面 | `(changes: { sourceId: string, changeEvent: ChangeEvent }[])` |
| `update-data` | 由 `createDataSourceManager` 在数据变化后发出,用于通知节点重新渲染 | `(newNodes: MNode[], sourceId: string, changeEvent: ChangeEvent, pageId: Id)` |
### 事件监听示例
```typescript
dataSourceManager.on('change', (dsId, changeEvent) => {
console.log(`数据源 ${dsId} 变更:`, changeEvent);
});
dataSourceManager.on('init', (data, errors) => {
console.log('所有数据源初始化完成', data, errors);
});
```