Redux 并不慢,只是你使用姿势不对 —— 一份优化指南
如何优化使用了 Redux 的 react 应用程序并不是显而易见的。但实际上相当简单。这是一个简短的指南,以及一些例子。
当优化使用 Redux 的 react 应用程序时,我经常听到有人说 Redux 缓慢。在99%的情况下,性能不佳的原因(适用于任何其他框架)与不必要的渲染相关联,因为 DOM 更新是昂贵的!在本文中,您将学习如何在使用 Redux 绑定 react 时避免不必要的 render
。
通常,我们使用官方 react 绑定的 Redux 的 connect 高阶组件, 当您的 Redux store
更新时,react 组件就会更新。这是一个将组件包装在另一个组件中的函数,该组件订阅了 Redux store
中的更改,并渲染 store
,这就是它的子节点更新的原因。
快速入坑 react-redux,官方 react 绑定 Redux
该 connect
高阶组件实际上是已经过优化。要了解如何最好地使用它,最好了解先它是如何工作的!
Redux 以及 react-redux 实际上是相当小的库,所以源代码并不费解。我鼓励大家阅读源代码,或至少其中的一些。如果你想进一步,编写自己的实现,它将让你深入了解为什么库的设计方式是这样的。
不用多说,我们来看看 react 绑定的工作原理。正如我们公认的那样,react 绑定的核心部分就是 connect
高阶组件,这是它的写法:
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
...extraOptions
} = {}
) {
...
}
作为附注 - 唯一的强制性参数是 mapStateToProps
,在大多数情况下,您只需要前两个参数。但是,我在这里使用完整的写法来说明 react bindings
是如何工作的。
传递给 connect
函数的所有参数都用于生成一个对象,该对象作为 props
传递到封装的组件上。mapStateToProps
用于将状态从 Redux store 映射到一个对象,mapDispatchToProps
用于生成包含函数的对象 - 通常这些函数是 actions
创建者。最后,mergeProps
有三个参数 stateProps
,dispatchProps
和 ownProps
。第一个是 mapStateToProps
的结果,第二个参数是 mapDispatchToProps
的结果,第三个参数是从组件本身继承的 props
对象。默认情况下,mergeProps
将这些参数简单地组合到一个对象中,但是如果您传递一个函数作为 mergeProps
参数,connect
则将使用该函数生成封装组件的 props
。
connect
函数的第四个参数是一个 options
对象。这包含5个选项:pure
可以是 true
或 false
,以及确定是否重新渲染组件的4个函数(应该返回一个布尔值)。pure
默认设置为true
。如果设置为false
,则connect
将跳过任何优化,并且options
对象中的4个函数将不生效。我个人不能想到一个用例,但是如果您希望关闭优化,则将其设置为false
。
我们的 mergeProps
函数产生的对象与最后一个 props
对象进行比较。如果我们的 connect
认为 props
对象已经改变了,那么这个组件就会重新渲染。要了解库如何决定是否有变更,我们可以查看该 shallowEqual
功能。如果函数返回 true
,组件将不会重新渲染,如果返回 false
,它将重新渲染。shallowEqual
执行此比较。下面你会看到 shallowEqual
方法的一部分,它告诉你所有你需要知道的:
for (let i = 0; i < keysA.length; i++) {
if (!hasOwn.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])) {
return false
}
}
总而言之,这是上面的代码:
它在 object a 中的 key 上循环,并检查 object B 是否拥有相同的属性。然后它检查 object A中的属性(具有相同名称)是否等于对象B的属性。如果只有一个比较返回false,那么对象将被视为不相等,并且会发生重新渲染。
这导致我们一个黄金法则:
只给您的组件它需要呈现的数据
这说得很含糊,所以让我们用一些实际的例子来阐述。
拆分连接的组件
我看到人们这样做 订阅容器组件一系列状态,并通过 props
传递所有东西。
const BigComponent = ({ a, b, c, d }) => (
<div>
<CompA a={a} />
<CompB b={b} />
<CompC c={c} />
</div>
);
const ConnectedBigComponent = connect(
({ a, b, c }) => ({ a, b, c })
);
现在,每次要么a,b或c变化,BigComponent包括CompA,CompB并且CompC将重新呈现。
相反,拆分您的组件,不要害怕更多地使用 connect:
const ConnectedA = connect(CompA, ({ a }) => ({ a }));
const ConnectedB = connect(CompB, ({ b }) => ({ b }));
const ConnectedC = connect(CompC, ({ c }) => ({ c }));
const BigComponent = () => (
<div>
<ConnectedA a={a} />
<ConnectedB b={b} />
<ConnectedC c={c} />
</div>
);
通过此次更新,CompA
将只有当a
已经改变才会重新渲染,CompB
当b
改变等。考虑这样一个场景,每一个值a
,b
并c
分别经常更新。对于每次更新,我们现在重新渲染一个,而不是所有组件。这几乎没有什么明显的三个组件,但如果你有更多!
改变你的状态,使其尽可能的最小
这是一个假设(稍微设计过)的例子:
你有一个大的项目列表,我们假设有300或更多。
<List>
{this.props.items.map(({ content, itemId }) => (
<ListItem
onClick={selectItem}
content={content}
itemId={itemId}
key={itemId}
/>
))}
</List>
当我们点击一个列表项时,一个动作被触发,更新一个存储值 - selectedItem
。每个列表项连接到 Redux 并获取selectedItem
:
const ListItem = connect(
({ selectedItem }) => ({ selectedItem })
)(SimpleListItem);
我们正在做正确的事情,我们仅将组件连接到所需的状态。但是,当selectedItem
更新时,所有ListItem
组件都将重新渲染,因为我们返回的对象selectedItem
已经更改。在此之前 { selectedItem: 123 }
,现在是 { selectedItem: 120 }
。
请记住,我们正在使用该 selectedItem
值来检查当前项目是否被选中。所以我们的组件真正需要知道的是它是否被选择 - 实质上是一个Boolean
。Boolean
是伟大的,因为只有两个可能的值,true
或者 false
。所以如果我们返回一个布尔值,而不是 selectedItem
,仅 Boolean
更改的两个项目将会重新渲染,这就是我们需要的。mapStateToProps
实际上 props
它是组件的第二个参数,我们可以用它来检查这是否实际上是选定的项目。以下是这样的:
const ListItem = connect(
({ selectedItem }, { itemId }) => ({ isSelected: selectedItem === itemId })
)(SimpleListItem);
现在,只要我们的 selectedItem
值发生变化,只有两个组件重新呈现 - ListItem
现在已经被选择了,而且原来那个已经被取消选择了。
保持数据扁平化
在 Redux文档提到 这是一种最佳实践。保持您的store 扁平化是有益的一些原因。但是对于本文的主旨,嵌套会造成问题,因为为了让我们的应用程序尽可能的快,我们希望我们的更新尽可能的细微。假设我们有一个这样的嵌套形状:
{
articles: [{
comments: [{
users: [{
}]
}]
}],
...
}
为了优化我们的 Article
,Comment
和 User
组件,我们现在需要所有的人订阅 articles
,然后深深伸入这种结构只返回他们所需要的状态。相反地,如此设计你的形状更有意义:
{
articles: [{
...
}],
comments: [{
articleId: ..,
userId: ...,
...
}],
users: [{
...
}]
}
然后使用映射函数选择注释和用户信息。关于这一点的更多信息可以在 Redux文档中阅读关于规范化状态的信息。
彩蛋:用于选择 Redux state 的库
这是完全可选的,取决于您。通常所有以上的建议都足够深入地让你写出快速的 react 和 Redux 应用程序。但是有两个优秀的库使得选择状态更容易一些:
Reselect 是 selectors
为您的 Redux 应用程序编写的引人注目的工具。从重新选择文档:
- 选择器可以计算派生数据,允许 Redux 存储尽可能小的状态。
- 选择器效率高。除非其中一个参数发生变化,否则不会重新计算选择器。
- 选择器是可组合的。它们可以用作其他选择器的输入。
对于具有复杂接口,复杂状态 和/或 频繁更新的应用程序,重新选择可以帮助您轻松使您的应用程序更快!
Ramda 是一个功能强大的库,具有更高阶的函数。换句话说 - 用函数创建函数。由于我们的映射函数只是这个函数,所以我们可以很方便地使用 Ramda
创建我们的选择器。Ramda
可以有选择器的所有功能并做的更多。查看 Ramda 食谱的一些例子,您可以使用 Ramda 做什么。