# 提升渲染性能
# 关于 redux 的渲染行为
我们每当发起一个 action 时,都会运行 useSelector,如果返回的是一个新的值,组件将会强制渲染。
如果我们在 useSelector 钩子中使用了 filter() 等函数,将会导致 useSelector 总会返回一个新的数组,那么每次调用 action 时,组件都将重新渲染,无论返回的数据有没有发生变化。
# 使用记忆化的 Selector
要解决上面的问题,我们就需要当数据未改变时获取的是上次数组的引用。
即“记忆”:保存之前的一组输入和计算的结果,如果输入相同,则返回之前的结果,而不是重新计算。
# createSelector 函数
features/posts/postsSlice.js
import { createSlice, createAsyncThunk, createSelector } from '@reduxjs/toolkit'
// omit slice logic
export const selectAllPosts = state => state.posts.posts
export const selectPostById = (state, postId) =>
state.posts.posts.find(post => post.id === postId)
export const selectPostsByUser = createSelector(
[selectAllPosts, (state, userId) => userId],
(posts, userId) => posts.filter(post => post.user === userId)
)
2
3
4
5
6
7
8
9
10
11
12
13
createSelector 参数:
将一个或多个“输入 selector ”函数作为第一个参数;
将一个 “输出 selector” 函数作为第二个参数,并且该函数的参数来自于 createSelector 的第一个参数。
我们尝试多次调用 selectPostsByUser
,它只会在 posts
或 userId
发生变化时重新执行输出 selector
记忆化的 selector 是提高 React + Redux 应用程序性能的宝贵工具,因为它们可以帮助我们避免不必要的重新渲染,并且如果输入数据没有更改,还可以避免执行潜在的复杂或昂贵的计算。
# 范式化数据
React 的默认行为是当父组件渲染时,React 会递归渲染其中的所有子组件!
所以当重新渲染一个组件时会导致其组件树的重新渲染。
一种方法是使用 React 提供的 memo 来缓存数据:
let PostExcerpt = ({ post }) => {
// omit logic
}
PostExcerpt = React.memo(PostExcerpt)
2
3
4
5
这确保了组件只有在 props 真正更改时才会重新渲染。
另一种方法是使用 redux Toolkit 提供的 createEntityAdapter 函数管理范式化数据。
# 什么是范式化?
我们一般将数据储存在数组中,当想要对数据通过 ID 字段来查找时,不得不使用遍历所有数据来获取 ID 对应项,当数据量较大时就比较耗时了。而一种无需检查所有其他项,直接根据其 ID 查找单个项的方法过程被称为“范式化”。
以下是一组“用户”对象的范式化 state 可能,如下所示:
{
users: {
ids: ["user1", "user2", "user3"],
entities: {
"user1": {id: "user1", firstName, lastName},
"user2": {id: "user2", firstName, lastName},
"user3": {id: "user3", firstName, lastName},
}
}
}
2
3
4
5
6
7
8
9
10
说明
类似于 map 结构
# createEntityAdapter
createEntityAdapter API 提供了一种将数据储存在 slice 中的标准方法,方法是获取项目集合并将它们放入 { ids: [], entities: {} }
的结构中。除了这个预定义的 state 结构,它还会生成一组知道如何处理该数据的 reducer 函数和 selector。
createEntityAdapter
接受一个选项对象,该对象可能包含一个 sortComparer
函数,该函数将用于通过比较两个项目来保持项目 id 数组的排序(工作方式与 array.sort()
相同)。
import {
createEntityAdapter
} from '@reduxjs/toolkit'
const postsAdapter = createEntityAdapter({
// 按照日期排序
sortComparer: (a, b) => b.date.localeCompare(a.date)
})
2
3
4
5
6
7
8
它返回一个对象,该对象包含 一组生成的 reducer 函数,用于操作实体 state 对象。这些 reducer 函数既可以用作特定 action 类型的 reducer,也可以用作 createSlice
中另一个 reducer 中的 "mutating" 实用函数。其中:
# getSelectors
可以传入一个 selector,它从 Redux 根 state 返回这个特定的 state slice,它会生成类似于 selectAll
和 selectById
的选择器。
export const {
selectAll: selectAllPosts,
selectById: selectPostById,
selectIds: selectPostIds
// Pass in a selector that returns the posts slice of state
} = postsAdapter.getSelectors(state => state.posts)
2
3
4
5
6
# getInitialState
生成一个空的 {ids: [], entities: {}}
对象。也可以传递更多字段给 getInitialState
,这些字段将会被合并。
const initialState = postsAdapter.getInitialState({
status: 'idle',
error: null
})
const postsSlice = createSlice({
name: 'posts',
initialState,
// omit
})
2
3
4
5
6
7
8
9
# upsertMany
const postsSlice = createSlice({
name: 'posts',
initialState,
// omit reducers
},
extraReducers: {
[fetchPosts.fulfilled]: (state, action) => {
state.status = 'succeeded'
// Add any fetched posts to the array
// Use the `upsertMany` reducer as a mutating update utility
postsAdapter.upsertMany(state, action.payload)
},
// Use the `addOne` reducer for the fulfilled case
[addNewPost.fulfilled]: postsAdapter.addOne
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当收到 fetchPosts.fulfilled
action 时,我们通过将现有 state
和 action.payload
传入 postsAdapter.upsertMany
函数,从而将所有传入的数据添加到 state 中。如果 action.payload
中有任何项目已经存在于我们 state 中,upsertMany
函数将根据匹配的 ID 将它们合并在一起。
总结
- 可用于优化性能的记忆化 selector 函数
- Redux Toolkit 重新导出了 Reselect 中的
createSelector
函数,该函数会生成记忆化的 selector - 只有当输入 selector 返回新的值时,记忆 selector 才会重新计算结果
- 记忆化可以跳过昂贵的计算,并确保返回相同的结果引用
- Redux Toolkit 重新导出了 Reselect 中的
- 可以使用多种方式来优化使用了 Redux 的 React 组件的渲染
- 避免在
useSelector
中创建新的对象/数组引用——这将导致不必要的重新渲染 - 可以传递记忆化的 selector 函数给
useSelector
来优化渲染 useSelector
可以接受比较函数,例如shallowEqual
,而不是引用相等- 组件可以包装在
React.memo()
中,仅在它们的 prop 发生变化时重新渲染 - 列表渲染可以通过让列表父组件仅读取每项的 ID 组成的数组、将 ID 传递给列表项子项并在子项中按 ID 检索项来实现优化
- 避免在
- 范式化 state 结构是存储项的推荐方法
- “范式化”意味着不重复数据,并通过 ID 将项目存储在查找表中
- 范式化 state 形式通常看起来像
{ids: [], entities: {}}
- Redux Toolkit 的
createEntityAdapter
API 帮助管理slice中的范式化数据- 通过传入
sortComparer
选项,可以按排序顺序保持项目 ID - adapter 对象包括:
adapter.getInitialState
,它可以接受额外的 state 字段,如加载 state- 预先创建通用 reducer,例如
setAll
、addMany
、upsertOne
和removeMany
adapter.getSelectors
,生成类似于selectAll
和selectById
的 selector
- 通过传入