Redux 并不慢,只是你使用姿势不对 —— 一份优化指南

Redux 并不慢,只是你使用姿势不对 —— 一份优化指南

如何优化使用了 Redux 的 react 应用程序并不是显而易见的。但实际上相当简单。这是一个简短的指南,以及一些例子。

当优化使用 Redux 的 react 应用程序时,我经常听到有人说 Redux 缓慢。在99%的情况下,性能不佳的原因(适用于任何其他框架)与不必要的渲染相关联,因为 DOM 更新是昂贵的!在本文中,您将学习如何在使用 Redux 绑定 react 时避免不必要的 render

通常,我们使用官方 react 绑定的 Reduxconnect 高阶组件, 当您的 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 有三个参数 statePropsdispatchPropsownProps。第一个是 mapStateToProps 的结果,第二个参数是 mapDispatchToProps 的结果,第三个参数是从组件本身继承的 props 对象。默认情况下,mergeProps 将这些参数简单地组合到一个对象中,但是如果您传递一个函数作为 mergeProps 参数,connect 则将使用该函数生成封装组件的 props

connect 函数的第四个参数是一个 options 对象。这包含5个选项:pure 可以是 truefalse,以及确定是否重新渲染组件的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已经改变才会重新渲染,CompBb改变等。考虑这样一个场景,每一个值abc分别经常更新。对于每次更新,我们现在重新渲染一个,而不是所有组件。这几乎没有什么明显的三个组件,但如果你有更多!

改变你的状态,使其尽可能的最小

这是一个假设(稍微设计过)的例子:

你有一个大的项目列表,我们假设有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 值来检查当前项目是否被选中。所以我们的组件真正需要知道的是它是否被选择 - 实质上是一个BooleanBoolean 是伟大的,因为只有两个可能的值,true 或者 false。所以如果我们返回一个布尔值,而不是 selectedItem ,仅 Boolean 更改的两个项目将会重新渲染,这就是我们需要的。mapStateToProps 实际上 props 它是组件的第二个参数,我们可以用它来检查这是否实际上是选定的项目。以下是这样的:

const ListItem = connect(
  ({ selectedItem }, { itemId }) => ({ isSelected: selectedItem === itemId })
)(SimpleListItem);

现在,只要我们的 selectedItem 值发生变化,只有两个组件重新呈现 - ListItem 现在已经被选择了,而且原来那个已经被取消选择了。

保持数据扁平化

Redux文档提到 这是一种最佳实践。保持您的store 扁平化是有益的一些原因。但是对于本文的主旨,嵌套会造成问题,因为为了让我们的应用程序尽可能的快,我们希望我们的更新尽可能的细微。假设我们有一个这样的嵌套形状:

{
  articles: [{
    comments: [{
      users: [{
      }]
    }]
  }],
  ...
}

为了优化我们的 ArticleCommentUser 组件,我们现在需要所有的人订阅 articles,然后深深伸入这种结构只返回他们所需要的状态。相反地​​,如此设计你的形状更有意义:

{
  articles: [{
    ...
  }],
  comments: [{
    articleId: ..,
    userId: ...,
    ...
  }],
  users: [{
    ...
  }]
}

然后使用映射函数选择注释和用户信息。关于这一点的更多信息可以在 Redux文档中阅读关于规范化状态的信息

彩蛋:用于选择 Redux state 的库

这是完全可选的,取决于您。通常所有以上的建议都足够深入地让你写出快速的 react 和 Redux 应用程序。但是有两个优秀的库使得选择状态更容易一些:

Reselectselectors 为您的 Redux 应用程序编写的引人注目的工具。从重新选择文档:

  • 选择器可以计算派生数据,允许 Redux 存储尽可能小的状态。
  • 选择器效率高。除非其中一个参数发生变化,否则不会重新计算选择器。
  • 选择器是可组合的。它们可以用作其他选择器的输入。

对于具有复杂接口,复杂状态 和/或 频繁更新的应用程序,重新选择可以帮助您轻松使您的应用程序更快!

Ramda 是一个功能强大的库,具有更高阶的函数。换句话说 - 用函数创建函数。由于我们的映射函数只是这个函数,所以我们可以很方便地使用 Ramda 创建我们的选择器。Ramda 可以有选择器的所有功能并做的更多。查看 Ramda 食谱的一些例子,您可以使用 Ramda 做什么。

0%