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

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


如何优化使用了 Redux 的 React 应用不是那么显而易见的,但其实又是非常简单直接的。本文即是一份带有若干示例的简短指南。

在优化使用了 Redux 的 React 应用的时候,我经常听人说 Redux 很慢。其实在 99% 的情况下,性能低下都和不必要的渲染有关(这一论断也适用于其他框架),因为 DOM 更新的代价是昂贵的。通过本文,你将学会如何在使用 Redux 的 React 应用中避免不必要的渲染。

一般来讲,要在 Redux store 更新的时候同步更新 React 组件,需要用到 React 和 Redux 的官方绑定库中的 connect 高阶组件。
connect 是一个将你的组件进行包裹的函数,它返回一个高阶组件,该高阶组件会监听 Redux store,当有状态更新时就重新渲染自身及其后代组件。

React 和 Redux 的官方绑定库 —— react-redux 快速入门

connect 高阶组件实际上已经被优化过了。为了理解如何更好地使用它,必须先理解它是如何工作的。

实际上,Redux 和 react-redux 都是非常小的库,因此其源码也并非高深莫测。我鼓励人们通读源码,或者至少读一部分。如果你想更进一步的话,可以自己实现一个,这能让你深入理解为什么它要作如此设计。

闲言少叙,让我们稍微深入地研究一下 react-redux 的工作机制。前面已经提过,react-redux 的核心是 connect 高阶组件,其函数签名如下:

return function connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  {
    pure = true,
    areStatesEqual = strictEqual,
    areOwnPropsEqual = shallowEqual,
    areStatePropsEqual = shallowEqual,
    areMergedPropsEqual = shallowEqual,
    ...extraOptions
  } = {}
) {
...
}

顺便说一下 —— 只有 mapStateToProps 这一个参数是必须的,而且大多数情况下只会用到前两个参数。此处我引用这个函数签名是为了阐明 react-redux 的工作机制。

所有传给 connect 函数的参数都用于生成一个对象,该对象则会作为属性传给被包裹的组件。mapStateToProps 用于将 Redux store 的状态映射成一个对象,mapDispatchToProps 用于产生一个包含函数的对象 —— 这些函数一般都是动作生成器(action creators)。mergeProps 则接收 3 个参数:statePropsdispatchProps 和 ownProps,前两个分别是 mapStateToProps 和 mapDispatchToProps 的返回结果,最后一个则是继承自组件本身的属性。默认情况下,mergeProps 会将上述参数简单地合并到一个对象中;但是你也可以传递一个函数给 mergePropsconnect 则会使用这个函数为被包裹的组件生成属性。

connect 函数的第四个参数是一个属性可选的对象,具体包含 5 个可选属性:一个布尔值pure 以及其他四个用于决定组件是否需要重新渲染的函数(应当返回布尔值)。pure 默认为 true,如果设为 false,connect 高阶组件则会跳过所有的优化选项,而且那四个函数也就不起任何作用了。我个人认为不太可能有这类应用场景,但是如果你想关闭优化功能的话可以将其设为 false。

mergeProps 返回的对象会和上一个属性对象作比较,如果 connect 高阶组件认为属性对象所有改变的话就会重新渲染组件。为了理解 react-redux 是如何判断属性是否有变化的,请参考 shallowEqual 函数。如果该函数返回 true,则组件不会渲染;反之,组件将会重新渲染。shallowEqual 负责进行属性对象的比较,下文是其部分代码,基本表明了其工作原理:

for (let i = 0; i < keysA.length; i++) {
  if (!hasOwn.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])) {
    return false
  }
}

概括来讲,这段代码做了这些工作:

遍历对象 A 中的所有属性,检查对象 B 中是否存在同名属性。然后检查 A 和 B 同名属性的属性值是否相等。如果这些检查有一个返回 false,则对象 A 和 B 便被认为是不等的,组件也就会重新渲染。

这引出一条黄金法则:

只给组件传递其渲染所必须的数据

这可能有点难以理解,所以让我们结合一些例子来细细分析一下。

将和 Redux 有连接的组件拆分开来

我见过很多人这样做:用一个容器组件监听一大堆状态,然后通过属性传递下去。

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 })
);

现在,一旦 ab 或 c 中的任何一个发生改变,BigComponent 以及 CompACompB 和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 发生改变后才会重新渲染,CompC 也是类似的。如果 abc 更新很频繁的话,那每次更新我们仅仅只是重新渲染一个组件而不是一下渲染三个。就这三个组件来讲区别可能不会很明显,但要是组件再多一些就比较明显了。

转变组件状态,使之尽可能地小

这里有一个人为构造(稍有改动)的例子:

你有一个很大的列表,比如说有 300 多个列表项:

<List>
  {this.props.items.map(({ content, itemId }) => (
    <ListItem
      onClick={selectItem}
      content={content}
      itemId={itemId}
      key={itemId}
    />
  ))}
</List>

点击一个列表项便会触发一个动作,同时更新 store 中的值 selectedItem。每一个列表项都通过 Redux 获取 selectedItem 的值:

const ListItem = connect(
  ({ selectedItem }) => ({ selectedItem })
)(SimpleListItem);

这里我们只给组件传递了其所必须的状态,这是对的。但是,当 selectedItem 发生变化时,所有 ListItem 都会重新渲染,因为我们从 selectedItem 返回的对象发生了变化,之前是 { selectedItem: 123 } 而现在是 { selectedItem: 120 }

记住一点,我们使用了 selectedItem 的值来检查当前列表项是否被选中了。但是实际上组件只需要知道它有没有被选中即可, 本质上就是个 Boolean。布尔值用在这里简直完美,因为它仅仅有 true 和 false 两种状态。如果我们返回一个布尔值而不是 selectedItem,那当那个布尔值发生改变时只有两个组件会被重新渲染,这正是我们期望的结果。mapStateToProps 实际上会将组件的 props 作为第二个参数,我们可以利用这一点来确定当前组件是否是被选中的那一项。代码如下:

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

如此一来,无论 selectedItem 如何变化,只有两个组件会被重新渲染 —— 当前选中的ListItem 和那个被取消选择的 ListItem

保持数据扁平

Redux 文档 中作为最佳实践提到了这点。保持 store 扁平有很多好处。但就本文而言,嵌套会造成一个问题,因为我们希望状态更新粒度尽量小以使应用运行尽量快。比如说我们有这样一种深浅套的状态:

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

为了优化 ArticleComment 和 User 组件,它们都需要订阅 articles,而后在层层嵌套的属性中找到所需要的状态。其实如果将状态展开成这样会更加合理:

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

之后用自己的映射函数获取评论和用户信息即可。更多关于状态扁平化的内容可以参阅Redux 文档

福利:两个选择 Redux 状态的库

这一部分完全是可选的。一般来讲上述那些建议足够你编写出高效的 react 和 Redux 应用了。但还有两个可以大大简化状态选择的库:

Reselect 是为 Redux 应用编写 selectors 所必不可少的工具。根据其官方文档:

  • Selectors 可以计算衍生数据,可以让 Redux 做到存储尽可能少的状态。
  • Selectors 是高效的,只有在某个参数发生变化时才被重新计算。
  • Selectors 是可组合的。它们可以用作其他 selectors 的输入。

对于界面复杂、状态繁多、更新频繁的应用,reselect 可以大大提高应用运行效率。

Ramda 是一个由许多高阶函数组成、功能强大的函数库。 换句话说,就是许多用于创建函数的函数。由于我们的映射函数也不过只是函数而已,所以我们可以利用 Ramda 方便地创建 selectors。Ramda 可以完成所有 selectors 可以完成的工作,而且还不止于此。Ramda cookbook 中介绍了一些 Ramda 的应用示例。





原文发布时间为:2017年7月18日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-11-03 17:59:08

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

央视的黄昏?老龄化粉丝簇拥 新媒体姿势不对

你还记得最近在央视上追过什么热播吗? 不记得?恭喜你!说明你还年轻.整个电视产业观众的老龄化已是业内公认的现状,而央视尤甚.这种趋势非近一两年才呈现.在2006年的"广电蓝皮书"中,就有这样的描述:"央视的强势观众群包括男性观众.45岁以上的中老年观众.个人月收入601-2600元之间各人群.干部等,弱势收视群为年轻观众.女性观众.低收入和高收入群体--" 老龄化粉丝们所簇拥的,正是内容.推广方式和收入增长均显出暮气的央视. 一.内容为王可不是这么玩的 1. &qu

Git 提交的正确姿势:Commit message 编写指南

Git 每次提交代码,都要写 Commit message(提交说明),否则就不允许提交. $ git commit -m "hello world" 上面代码的-m参数,就是用来指定 commit mesage 的. 如果一行不够,可以只执行git commit,就会跳出文本编译器,让你写多行. $ git commit 基本上,你写什么都行(这里,这里和这里). 但是,一般来说,commit message 应该清晰明了,说明本次提交的目的. 目前,社区有多种 Commit mes

三张图读懂Greenplum在企业的正确使用姿势

背景 很多使用数据仓库的朋友可能都有过这样的困惑,为什么数据仓库的资源经常会出现不可控,或者抢用的情况,严重的甚至影响正常的作业任务,导致不能按时输出报表或者分析结果. 这里的原因较多,最主要的原因可能还是使用姿势不对,MPP是用极资源的产品,一伙人在抢资源当然跑不好.你想想一个跑道能让多架飞机同时起飞或降落吗? 第一张 老外通常如何使用数据仓库 数据仓库的使用人员通常是数据分析师,一个成熟的分析模型的建立,可能需要多次的数据模型分析试错. 通常试错不会允许直接在任务库中执行,因为很容易干扰任务

济南市民“遭遇”颈椎病感慨看电视“姿势”学问大

近日,伴随一轮又一轮的冷空气降临,全国气温大幅下降,这给市民的外出生活带来不少困扰."因为天冷室外活动受限,所以回到家唯一的娱乐活动就是看电视,每天饭后就躺在床上看电视,直到颈椎病犯了才意识到原来看电视姿势很关键."海尔电视新浪微博#看电视有姿势#活动中,济南市民刘先生以自己的切身经历,向大家讲述了"看电视姿势"的大学问.活动链接:http://t.cn/8DFakjQ"看电视姿势"学问大进入互联网时代,人们的生活被智能手机.PAD.电脑影响,躺

养生警惕:六种伤身体的坏姿势你有几个

打电话. 长时间"煲电话粥"的人要付出健康代价.很多人习惯用脖颈和肩部夹着电话,这样会诱发头痛.打电话时也要挺胸抬头,可以站在镜子前,检查自己的姿势是否恰当.为缓解脖颈部肌肉压力,建议做如下运动:倾斜头部使左耳靠近左肩,左胳膊上扬,左手落在头部右侧.拉伸脖颈,使右手垂直于地板.保持一会换另一只肩膀.[page] 背包. 长时间背大包会导致背部变形,两个肩膀经常轮流替换着背包,可以缓解压力.如果你背了10分钟的包,就感到肩膀和脖颈酸痛,那么该给你的包"减负"了. [p

Pycharm上python和unittest两种姿势傻傻分不清楚

前言 经常有人在群里反馈,明明代码一样的啊,为什么别人的能出报告,我的出不了报告:为什么别人运行结果跟我的不一样啊... 这种问题先检查代码,确定是一样的,那就是运行姿势不对了,一旦导入unittest模块,pycharm会自动识别以unittest的姿势去运行了.   一.unittest运行单个用例 1.如下代码,如果我只想运行其中的一个,如test1,如何运行呢? 2.如果想运行哪个用例,鼠标放到对应的区域,右键就能直接运行单个用例了 3.注意上图红色框框,显示的是Run 'Unittes

从setTimeout谈JavaScript运行机制

前言 最近在看些JavaScript异步的东西,但是由于时间有限,才刚看了个头,不得不中途停止.为了方便日后查阅以备重拾,遂记录一点体会,如果能使得他人有所收获,那更是极好的.其实本文与异步并没有太大关系. 从setTimeout说起 众所周知,JavaScript是单线程的编程,什么是单线程,就是说同一时间JavaScript只能执行一段代码,如果这段代码要执行很长时间,那么之后的代码只能尽情地等待它执行完才能有机会执行,不像人一样,人是多线程的,所以你可以一边观看某岛国动作片,一边尽情挥洒汗

倾斜的鼠标翻转导航制作上的烦恼

前天网上有个朋友发给我一个页面让我帮她看一下为什么鼠标翻转实现不了.我打开源文件看了一下,发现她根本没有掌握一个鼠标翻转的特性.并且对于倾斜导航的思考也不足.虽然我当时看出来了这些问题,但是由于手上一个项目正在收尾一时也没有时间向她一个讲解.正好昨天工作忙完了,现在又拿起那个文件看了一下,发现能过这个事件可以讲解好多个知识点,有一些地方比较容易让人不太注意,但是确实是非常关键的.下面我们通过制作一个倾斜的鼠标翻转导航为过程来针对不同的地方做出一些提示,希望可以帮助一些对于鼠标翻转导航制作上还存在

10 款简化工作流程的运营工具

  520表白日:想要做个好运营,不会撩妹怎么行? 一年一度的520表白日来了,这一天不向男神/女神表白,恐怕又要苦逼单身一年了.没有一点特别的撩妹神技怎能顺利脱单? 俗话说,直男撩妹,姿势不对.不如趁早补课,比如,用运营产品的方式撩妹.运营产品和撩妹在思路上没啥区别:首先找到你心仪的妹子(目标用户),获取她的基本信息(用户画像),了解她的喜好和需求(客户痛点),时刻关注妹子周围的动向(市场研究),潜入她的圈子形成影响力(社群营销),获取有用的信息(数据分析),做到妹子还没开口,就已经了解妹子的