跳到主要内容

v0.14:精简的 Manager API、性能提升与 GC

· 阅读需 13 分钟
Nathaniel Tucker
Creator of Reactive Data Client

Manager:

Manager API 得到了精简——更扁平的 action 结构降低了代码复杂度,并通过减少不同的内联缓存条目来提升性能;同时更简单的配置 API 让自定义更加愉快。

性能:

Schema/数据建模:

测试:

垃圾回收:

  • GCPolicy——自动回收过时数据

破坏性变更:

Manager​

Manager API 得到了精简——更扁平的 action 结构降低了代码复杂度,并通过减少不同的内联缓存条目来提升性能;同时更简单的配置 API 让自定义更加愉快。

Action 结构​

Action 结构更简单、更一致。#3143、#3139

  • action.meta.args -> action.args
  • action.meta.key -> action.key
  • action.meta.nm 已移除(未使用)
  • action.payload -> action.response(针对 SET_RESPONSE)
Before
{
"type": "rdc/setresponse",
"meta": {
"key": "PATCH https://jsonplaceholder.typicode.com/todos/1",
"payload": {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": true
},
"args": [
{ "id": 1 },
{ "completed": true }
],
"nm": true,
"endpoint": {
"name": "Todo.partialUpdate",
"schema": {
"key": "Todo",
"schema": {}
},
"sideEffect": true,
},
"fetchedAt": "12:44:24.206 PM",
"date": "12:44:24.334 PM",
"expiresAt": "1:44:24.334 PM"
},
"error": false
}
After
{
"type": "rdc/setresponse",
"key": "PATCH https://jsonplaceholder.typicode.com/todos/1",
"response": {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": true
},
"args": [
{ "id": 1 },
{ "completed": true }
],
"endpoint": {
"name": "Todo.partialUpdate",
"schema": {
"key": "Todo",
"schema": {}
},
"sideEffect": true,
},
"meta": {
"fetchedAt": "12:44:24.206 PM",
"date": "12:44:24.334 PM",
"expiresAt": "1:44:24.334 PM"
},
"error": false
}

Manager.middleware​

Manager 现在可以使用 middleware 属性来代替 getMiddleware() 方法。#3164

Before
class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
// handle action
return next(action);
};

cleanup() {}
}
After
class MyManager implements Manager {
middleware: Middleware = controller => next => async action => {
// handle action
return next(action);
};

cleanup() {}
}
提示

为了向后兼容,getMiddleware() 仍然可用。

不带 _TYPE 后缀的 actionTypes​

现在提供了更简洁的 action 类型名称(非破坏性变更——旧名称同样保留)。#3244

Before
import type { Manager, Middleware } from '@data-client/react';
import { actionTypes } from '@data-client/react';

export default class LoggingManager implements Manager {
middleware: Middleware = controller => next => async action => {
switch (action.type) {
case actionTypes.SET_RESPONSE_TYPE:
console.info(
`${action.endpoint.name} ${JSON.stringify(action.response)}`,
);
default:
return next(action);
}
};

cleanup() {}
}
After
import type { Manager, Middleware } from '@data-client/react';
import { actionTypes } from '@data-client/react';

export default class LoggingManager implements Manager {
middleware: Middleware = controller => next => async action => {
switch (action.type) {
case actionTypes.SET_RESPONSE:
console.info(
`${action.endpoint.name} ${JSON.stringify(action.response)}`,
);
default:
return next(action);
}
};

cleanup() {}
}

getDefaultManagers()​

getDefaultManagers() 现在接受配置选项。#3161

import { getDefaultManagers } from '@data-client/react';

// Remove DevToolsManager
const managers = getDefaultManagers({ devToolsManager: null });

// Configure DevToolsManager
const managers = getDefaultManagers({
devToolsManager: {
latency: 1000,
predicate: (state, action) => action.type !== actionTypes.SET_RESPONSE,
},
});

// Use custom NetworkManager
const managers = getDefaultManagers({
networkManager: new IdlingNetworkManager(),
});

NetworkManager 构造函数​

为了更清晰,NetworkManager 的构造函数现在使用关键字参数。

Before
new NetworkManager(42, 7);
After
new NetworkManager({ dataExpiryLength: 42, errorExpiryLength: 7 });

性能​

React 18+ 的并发模式和自动批处理改变了我们应该采用的优化方式。等待空闲回调不再是最优选择——现在立即发起获取能带来更好的结果。内部改动利用闭包和 Map 数据结构来加快规范化和查找速度,同时移除臃肿的 polyfill 依赖显著缩小了包体积。

立即获取​

NetworkManager 现在会立即发起获取,而不是等待空闲回调。#3146

这让数据加载更快,对于渲染树复杂的应用尤其明显。

对于更喜欢之前行为的用户,IdlingNetworkManager 仍然可用:

import { IdlingNetworkManager, getDefaultManagers } from '@data-client/react';

const managers = getDefaultManagers({
networkManager: new IdlingNetworkManager(),
});

包体积​

移除臃肿的 polyfill 包后,各个包现在缩小了 30%。#3384

此外,polyfill 不再污染全局作用域。#3353

规范化性能​

内部 schema 接口的改动带来了 10% 的规范化性能提升。#3134

使用 Map 代替普通对象来存储实体,在大型数据集上查找速度最多可快 32%。#3390

历史基准测试 | 查看基准测试

Schema/数据建模​

Schema API 减少了常见模式的样板代码,同时支持更高级的用例。客户端连接现在可以跨任意实体工作(不再局限于嵌套关系),动态失效可以处理仅在服务端存在的删除逻辑,而像 Entity.pk() 默认使用 id 这样合理的默认值则消除了重复代码。

Query 连接​

Query 现在可以接受 Object Schema,从而实现跨多种实体类型的连接。#3165

class Ticker extends Entity {
product_id = '';
price = 0;
pk() { return this.product_id; }
}

class Stats extends Entity {
product_id = '';
last = 0;
pk() { return this.product_id; }
}

// Join Ticker and Stats by product_id
const queryPrice = new schema.Query(
{ ticker: Ticker, stats: Stats },
({ ticker, stats }) => ticker?.price ?? stats?.last,
);

// Use in components
const price = useQuery(queryPrice, { product_id: 'BTC-USD' });

EntityMixin​

新增 EntityMixin,用于将 Entity 行为与现有的类组合。#3243

import { EntityMixin } from '@data-client/rest';

// Your existing class
export class Article {
id = '';
title = '';
content = '';
tags: string[] = [];
}

// Add Entity behavior
export class ArticleEntity extends EntityMixin(Article) {}

Entity.pk() 默认值​

Entity.pk() 现在默认返回 this.id。#3188

Before
class Todo extends Entity {
id = '';
title = '';
completed = false;

pk() {
return this.id;
}
}
After
class Todo extends Entity {
id = '';
title = '';
completed = false;
}

动态失效​

在 Entity.process() 中返回 undefined,即可根据响应数据动态地让实体失效。#3407

class PriceLevel extends Entity {
price = 0;
amount = 0;

pk() { return `${this.price}`; }

static process(input: [number, number], parent: any, key: string | undefined) {
const [price, amount] = input;
// Invalidate when amount is 0
if (amount === 0) return undefined;
return { price, amount };
}
}

resource()​

为求简洁,createResource() 已重命名为 resource()。#3158

Before
import { createResource } from '@data-client/rest';

const TodoResource = createResource({
path: '/todos/:id',
schema: Todo,
});
After
import { resource } from '@data-client/rest';

const TodoResource = resource({
path: '/todos/:id',
schema: Todo,
});
备注

为了向后兼容,createResource 仍然会导出。

测试​

测试应当简单。新的辅助函数使用合理的默认值,消除常见测试场景中的样板代码。

renderDataHook()​

新增的 renderDataHook() 使用默认的 DataProvider,消除了样板代码。#3238

此外,为保持一致,makeRenderDataClient() 已重命名为 makeRenderDataHook()(旧名称仍然可用)。

import { renderDataHook } from '@data-client/test';

const { result } = renderDataHook(
() => useSuspense(ArticleResource.get, { id: 5 }),
{
initialFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'Test' },
},
],
},
);

垃圾回收​

长时间运行的应用会在内存中积累过时数据。新的垃圾回收策略使用引用计数来跟踪哪些数据正被组件绑定,从而自动清理已过期的条目。

GCPolicy​

GCPolicy 可以自动对过时数据进行垃圾回收。#3343

import { GCPolicy, DataProvider } from '@data-client/react';

// Run GC sweep every 10 minutes
<DataProvider gcPolicy={new GCPolicy({ intervalMS: 60 * 1000 * 10 })}>
{children}
</DataProvider>

配置过期行为:

new GCPolicy({
intervalMS: 60 * 1000 * 5, // How often to run GC
expiryMultiplier: 2, // How many stale lifetimes before GC
})

使用 ImmortalGCPolicy 可以禁用垃圾回收(即之前的默认行为):

import { ImmortalGCPolicy, DataProvider } from '@data-client/react';

<DataProvider gcPolicy={new ImmortalGCPolicy()}>
{children}
</DataProvider>

其他改进​

  • Collection 可与 Union 等多态 schema 配合使用 #3151
  • Collection 支持嵌套参数(兼容 qs 库)#3281
  • Interceptor 对 Manager 派发的 action 同样生效 #3365
  • NetworkError 的消息中包含 URL,便于调试
  • React Native:使用 InteractionManager.runAfterInteractions 减少掉帧 #3127
  • React Native:package.json exports 中新增 react-native 入口 #3371
  • 改进了 React 19 兼容性 #3279

迁移指南​

本次升级需要同时更新所有包的版本。

npm install --save @data-client/react@^0.14.0 @data-client/rest@^0.14.0 @data-client/test@^0.14.0 @data-client/img@^0.14.0

针对所有用户​

NetworkManager 立即发起获取​

NetworkManager 现在会立即发起获取,而不是等待空闲。这提升了在 React 18+ 下的性能。#3146

若要保留之前的行为,请使用 IdlingNetworkManager:

import { IdlingNetworkManager, getDefaultManagers } from '@data-client/react';

const managers = getDefaultManagers({
networkManager: new IdlingNetworkManager(),
});

针对自定义 Manager 作者​

如果你没有自定义 Manager,可以跳过本节。

action.payload -> action.response​

对于处理 SET_RESPONSE 的自定义 Manager:#3141

Before
import {
SET_RESPONSE_TYPE,
type Manager,
type Middleware,
} from '@data-client/react';

export default class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
switch (action.type) {
case SET_RESPONSE_TYPE:
console.log('Resolved with value', action.payload);
return next(action);
default:
return next(action);
}
};

cleanup() {}
}
After
import {
SET_RESPONSE_TYPE,
type Manager,
type Middleware,
} from '@data-client/react';

export default class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
switch (action.type) {
case SET_RESPONSE_TYPE:
console.log('Resolved with value', action.response);
return next(action);
default:
return next(action);
}
};

cleanup() {}
}

fetchAction.payload 已移除​

对于处理 FETCH 的自定义 Manager:#3141

Before
import {
FETCH_TYPE,
type Manager,
type Middleware,
} from '@data-client/react';

export default class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
switch (action.type) {
case FETCH_TYPE:
// consume fetch, and print the resolution
action.payload().then(response => console.log(response));
default:
return next(action);
}
};

cleanup() {}
}
After
import {
FETCH_TYPE,
type Manager,
type Middleware,
} from '@data-client/react';

export default class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
switch (action.type) {
case FETCH_TYPE:
// consume fetch, and print the resolution
action
.endpoint(...action.args)
.then(response => console.log(response));
default:
return next(action);
}
};

cleanup() {}
}

action.meta.args -> action.args​

Action 参数从 meta 移到了顶层。#3143

Before
const { args, key } = action.meta;
After
const { args, key } = action;

针对自定义 Schema 作者​

如果你没有实现 normalize() 或 denormalize() 的自定义 schema,可以跳过本节。

Schema 接口变更​

normalize() 和 denormalize() 的接口已经改变。#3134

这为所有用户带来了 10% 的性能提升。

Schema.normalize visit()​

visit() 接口现在移除了与上下文无关的参数:

/** Visits next data + schema while recurisvely normalizing */
export interface Visit {
(schema: any, value: any, parent: any, key: any, args: readonly any[]): any;
creating?: boolean;
}
Before
processedEntity[key] = visit(
processedEntity[key],
processedEntity,
key,
this.schema[key],
addEntity,
visitedEntities,
storeEntities,
args,
);
After
processedEntity[key] = visit(
this.schema[key],
processedEntity[key],
processedEntity,
key,
args,
);

这些参数所需的信息,现在通过让 visit() 对它们形成闭包来提供。

Schema.normalize 接口​

从直接访问数据改为使用 getEntity 等函数:

interface SchemaSimple {
normalize(
input: any,
parent: any,
key: any,
args: any[],
visit: (
schema: any,
value: any,
parent: any,
key: any,
args: readonly any[],
) => any,
addEntity: (...args: any) => any,
getEntity: (...args: any) => any,
checkLoop: (...args: any) => any,
): any;
}

我们还新增了 checkLoop(),它把 Entity 中的部分逻辑移到了核心的规范化算法中。

/** Returns true if a circular reference is found */
export interface CheckLoop {
(entityKey: string, pk: string, input: object): boolean;
}

Schema.denormalize unvisit()​

unvisit 参数现在把 schema 作为第一个参数:

interface SchemaSimple {
denormalize(
input: {},
args: readonly any[],
unvisit: (schema: any, input: any) => any,
): T;
}

升级支持​

一如往常,如果你遇到任何问题或有疑问,欢迎加入我们的 Chat 或提交 bug