关于「你不知道的 Redux」这个专栏
在前端进阶中,redux 或者说是状态管理 是一个比较好的切入点,在实际项目中应用广泛,可选的解决方案非常多,生态非常丰富,学习他可以帮助我们提升编程设计思想,而且代码非常易读,代码量也不大,不像 webpack,react 源码那样庞大,学习收益非常高,同时也是面试中的高频考点。
专栏会持续更新 redux 整个生态的相关知识,包括 react-redux ,redux 中间件,immer 等等,甚至会考虑其他的解决方案,比如最近的 zustand 等。
什么是 Redux 范式化
Redux 范式化是一种将嵌套的 State 数据结构转换为扁平化的数据结构的方法,以便更轻松地管理和更新应用程序的状态。它通过将嵌套的数据结构拆分为多个单独的对象,并使用唯一的 ID 来引用它们,从而实现了数据的扁平化。
我们用代码具体解释一下: 假设后端返回我们一段博客文章的数据,数据结构如下:
const blogPosts = [
{
id: "post1",
author: { username: "user1", name: "User 1" },
body: "......",
comments: [
{
id: "comment1",
author: { username: "user2", name: "User 2" },
comment: ".....",
},
{
id: "comment2",
author: { username: "user3", name: "User 3" },
comment: ".....",
},
],
},
{
id: "post2",
author: { username: "user2", name: "User 2" },
body: "......",
comments: [
{
id: "comment3",
author: { username: "user3", name: "User 3" },
comment: ".....",
},
{
id: "comment4",
author: { username: "user1", name: "User 1" },
comment: ".....",
},
{
id: "comment5",
author: { username: "user3", name: "User 3" },
comment: ".....",
},
],
},
// and repeat many times
];
上面非范式化数据在前端展示和更新上可能存在如下问题:
- 数据冗余较多,例如存在多个 author 对象,如果 author 对象中的某个 name 发生变化,那么需要遍历所有的 post 对象进行更新,如果有遗漏就可能会出现数据不一致的情况。
- 数据嵌套较深,如果我们要更新某条评论,我们需要先找到对应的 post,然后再找到对应的 comment,最后才能更新 comment 的内容
- 因为 redux 的 store 是不可变的,加入我们更新某条评论,会造成整个 post 组件的更新,更有甚者会造成整个 postList 重新渲染,这样会造成不必要的性能损耗。
Redux 范式化有哪些优点
接下来我们看一下范式化的数据结构:
{
posts : {
byId : {
"post1" : {
id : "post1",
author : "user1",
body : "......",
comments : ["comment1", "comment2"]
},
"post2" : {
id : "post2",
author : "user2",
body : "......",
comments : ["comment3", "comment4", "comment5"]
}
},
allIds : ["post1", "post2"]
},
comments : {
byId : {
"comment1" : {
id : "comment1",
author : "user2",
comment : ".....",
},
"comment2" : {
id : "comment2",
author : "user3",
comment : ".....",
},
"comment3" : {
id : "comment3",
author : "user3",
comment : ".....",
},
"comment4" : {
id : "comment4",
author : "user1",
comment : ".....",
},
"comment5" : {
id : "comment5",
author : "user3",
comment : ".....",
},
},
allIds : ["comment1", "comment2", "comment3", "comment4", "comment5"]
},
users : {
byId : {
"user1" : {
username : "user1",
name : "User 1",
},
"user2" : {
username : "user2",
name : "User 2",
},
"user3" : {
username : "user3",
name : "User 3",
}
},
allIds : ["user1", "user2", "user3"]
}
}
这样的数据结构在实际使用中,会有如下便利:
- 数据冗余较少,不会出现数据不一致的情况
- 数据更加扁平,数据查找和更新更加方便
- List 组件和 Item 组件之间可以通过 id 传值,不需要通过 props 传递整个对象,较少可能不必要的渲染
其实,我们可以把范式化
类比为关系型数据库中的表,而非范式化
类比为非关系型数据库中的文档。范式化的数据结构更加适合前端的展示和更新,而非范式化的数据结构更加适合后端的存储和查询。
id
为关系型数据库中的主键,byId
为关系型数据库中的行,allIds
为关系型数据库中的主键列。
如下表中多个实体之间的关系,就类似于关系型数据库之间 一对多,多对多的关系。
{
entities: {
authors : { byId : {}, allIds : [] },
books : { byId : {}, allIds : [] },
authorBook : {
byId : {
1 : {
id : 1,
authorId : 5,
bookId : 22
},
2 : {
id : 2,
authorId : 5,
bookId : 15,
},
3 : {
id : 3,
authorId : 42,
bookId : 12
}
},
allIds : [1, 2, 3]
}
}
}
Redux Toolkit 数据范式化的最佳实践
接下来我们来看一下,RTK 如何实现数据范式化的最佳实践。
在 RTK 中,使用createEntityAdapter
来创建一个entityAdapter
对象,来实现数据范式化。
首先看一下 entityAdapter 的范式化数据结构定义:
export interface EntityState<T> {
ids: readonly EntityId[];
entities: Record<EntityId, T>;
}
{
users: {
ids: ["user1", "user2", "user3"],
entities: {
"user1": {id: "user1", firstName, lastName},
"user2": {id: "user2", firstName, lastName},
"user3": {id: "user3", firstName, lastName},
}
}
}
查看下面简单的例子, 如果对 rtk 不熟悉的同学,可以先看一下我之前的文章Redux Toolkit 可能是目前 Redux 的最佳实践
在下面代码中,我们使用createEntityAdapter
创建了一个usersAdapter
对象,然后使用usersAdapter.getInitialState()
创建了一个初始的 state,然后在reducers
中,我们使用usersAdapter.addOne
来添加一条数据,usersAdapter.setAll
来添加多条数据,usersAdapter.updateOne
来更新一条数据,usersAdapter.removeOne
来删除一条数据 。我们对常见的增删改查操作,都进行了封装,做了大量简化。
同时我们可以通过usersAdapter.getSelectors
来创建一个selectors
对象,然后通过selectAllUsers
来获取所有的用户数据,通过selectUserById
来获取指定 id 的用户数据。
const usersAdapter = createEntityAdapter();
const initialState = usersAdapter.getInitialState();
export const fetchUsers = createAsyncThunk("users/fetchUsers", async () => {
const response = await client.get("/fakeApi/users");
return response.data;
});
const usersSlice = createSlice({
name: "users",
initialState,
reducers: {
userAdded: usersAdapter.addOne, //往 users 中添加一条数据,ids添加一个ID,entities添加一条数据
userUpdated: usersAdapter.updateOne, //更新一条数据
userRemoved: usersAdapter.removeOne, //删除一条数据
},
extraReducers(builder) {
builder.addCase(fetchUsers.fulfilled, usersAdapter.setAll);
},
});
export default usersSlice.reducer;
export const { selectAll: selectAllUsers, selectById: selectUserById } =
usersAdapter.getSelectors((state) => state.users);
接下来我们看看createEntityAdapter
相关技术实现
- 接受一个
options
对象,options
对象中包含selectId
和sortComparer
两个属性,selectId
用来获取实体的id
,sortComparer
用来对实体进行排序。 - 比较关键的是 selectorsFactory 和 stateAdapter,selectorsFactory 用来创建 selectors 对象,stateAdapter 用来创建 state 对象。
export function createEntityAdapter<T>(
options: {
selectId?: IdSelector<T>;
sortComparer?: false | Comparer<T>;
} = {}
): EntityAdapter<T> {
const { selectId, sortComparer }: EntityDefinition<T> = {
sortComparer: false,
selectId: (instance: any) => instance.id,
...options,
};
const stateFactory = createInitialStateFactory<T>();
const selectorsFactory = createSelectorsFactory<T>();
const stateAdapter = sortComparer
? createSortedStateAdapter(selectId, sortComparer)
: createUnsortedStateAdapter(selectId);
return {
selectId,
sortComparer,
...stateFactory,
...selectorsFactory,
...stateAdapter,
};
}
这里我们不考虑排序的情况, 我们来看一下createUnsortedStateAdapter
的实现。代码实现简单易懂
- 接受 selectId 方法,用来获取实体的 id
- addOneMutably 方法,用来往 ids 数组中添加一个 id,往 entities Object 中添加一条数据
- 返回之前嵌套的 createStateOperator 方法为 immer 的 produce 方法,用来创建一个 reducer 函数,用来更新 state(后续有计划展开聊聊 immer)
export function createUnsortedStateAdapter<T>(
selectId: IdSelector<T>
): EntityStateAdapter<T> {
type R = EntityState<T>;
function addOneMutably(entity: T, state: R): void {
const key = selectIdValue(entity, selectId);
if (key in state.entities) {
return;
}
state.ids.push(key);
state.entities[key] = entity;
}
function addManyMutably(
newEntities: readonly T[] | Record<EntityId, T>,
state: R
): void {
newEntities = ensureEntitiesArray(newEntities);
for (const entity of newEntities) {
addOneMutably(entity, state);
}
}
return {
addOne: createStateOperator(addOneMutably),
addMany: createStateOperator(addManyMutably),
};
}
我们再看看 createSelectorsFactory
的实现
- createDraftSafeSelector 是一个封装了 immer 和 reselect 的 createSelector 方法,接受多个 selector 方法,其中,最后一个 selector 方法是一个回调函数,把前面 selector 方法作为入参数,最后返回一个新的数据。
- 拿 selectIds 举例,selectState 先从整个 store 中获取到 users 的 state,然后再从 users 的 state 中获取到 ids
- selectAll 稍微复杂一些,还是先经过 selectState,然后再从 users 的 state 中获取到 ids 和 entities,然后再根据 ids 和 entities 获取到所有的数据的数组结构
export function createSelectorsFactory<T>() {
function getSelectors<V>(
selectState?: (state: V) => EntityState<T>
): EntitySelectors<T, any> {
const selectIds = (state: EntityState<T>) => state.ids;
const selectEntities = (state: EntityState<T>) => state.entities;
const selectAll = createDraftSafeSelector(
selectIds, // 从 state 中获取 ids
selectEntities, // 从 state 中获取 entities
(ids, entities): T[] => ids.map((id) => entities[id]!) // 根据 ids 和 entities 获取所有的数据的数组结构
);
const selectId = (_: unknown, id: EntityId) => id;
const selectById = (entities: Dictionary<T>, id: EntityId) => entities[id];
const selectTotal = createDraftSafeSelector(selectIds, (ids) => ids.length);
const selectGlobalizedEntities = createDraftSafeSelector(
selectState as Selector<V, EntityState<T>>,
selectEntities
);
return {
selectIds: createDraftSafeSelector(selectState, selectIds),
selectEntities: selectGlobalizedEntities,
selectAll: createDraftSafeSelector(selectState, selectAll),
selectTotal: createDraftSafeSelector(selectState, selectTotal),
selectById: createDraftSafeSelector(
selectGlobalizedEntities,
selectId,
selectById
),
};
}
return { getSelectors };
}
总结一下
本文主要介绍了 Redux 范式化的好处,可以减少数据冗余,可以提高数据的查询效率,同时可以减少 UI 层重复渲染。
然后,我们介绍了在 RTK 中 通过 createEntityAdapter 落地 Redux 范式化, 大大提升了我们的开发效率,同时也减少了很多 reducer 和 selector 模版代码, 非常推荐大家使用。
最后,我们对 createEntityAdapter 相关的技术实现进行了简单的分析,希望能够帮助大家更好的理解 createEntityAdapter 的实现原理。
如果觉得对你的理解有帮助,请点赞收藏,后续会继续聊聊 Redux 生态相关的技术实现。
如果对于部分内容不理解,可以在评论区留言,我会尽快回复。