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

  • 原文地址:Redux 并不慢,只是你使用姿势不对 —— 一份优化指南
  • 原文作者:Julian Krispel
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:reid3290
  • 校对者:sunui,xekri

如何优化使用了 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 个参数:statePropsdispatchPropsownProps,前两个分别是 mapStateToPropsmapDispatchToProps 的返回结果,最后一个则是继承自组件本身的属性。默认情况下,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 })
);复制代码

现在,一旦 abc 中的任何一个发生改变,BigComponent 以及 CompACompBCompC 都会重新渲染。

其实应该将组件拆分开来,而无需过分担心使用了太多的 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 }) => (<ListItemonClick={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。布尔值用在这里简直完美,因为它仅仅有 truefalse 两种状态。如果我们返回一个布尔值而不是 selectedItem,那当那个布尔值发生改变时只有两个组件会被重新渲染,这正是我们期望的结果。mapStateToProps 实际上会将组件的 props 作为第二个参数,我们可以利用这一点来确定当前组件是否是被选中的那一项。代码如下:

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

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

保持数据扁平

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

{articles: [{comments: [{users: [{}]}]}],...
}复制代码

为了优化 ArticleCommentUser 组件,它们都需要订阅 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 的应用示例。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/257967.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

把windows装到linux下,如何将WSL(Windows Subsystem for Linux 2)安装到Windows 10?

原标题&#xff1a;如何将WSL(Windows Subsystem for Linux 2)安装到Windows 10&#xff1f;Windows 10凭借大受欢迎的WSL(Windows Subsystem for Linux)进入Linux领域。由于最近推出了WSL的最新版WSL2&#xff0c;用户现在可以利用实际的Linux内核从Windows执行Linux任务。现在…

TWRP-recovery中文界面安装方法[转]

把下载到的ui.zip放入sdcard1/twrp文件夹。注意&#xff0c;是内置存储卡中。如没有上述文件夹&#xff0c;自行建立后通过文件管理器放入&#xff0c;不是卡刷。文件夹应如下所示&#xff1a;sdcard1&#xff08;内置SD&#xff09; &#xff5c; ┕--twrp&#xff08;文件夹…

如何定期备份网站数据

产生这个问题的背景是我在维护两个个人的网站&#xff0c;因为采用的是虚拟主机&#xff0c;有时候空间续费不及时等&#xff0c;都可能造成数据的丢失&#xff0c;为了保障数据不丢失&#xff0c;因为有必要每15天左右对网站数据进行备份以防止发生不当的事情。 我们希望做的就…

初创团队可能不适合应届生小孩

根据最近招聘中接触到的一些刚毕业小孩的表现&#xff0c;谈谈这个问题&#xff1a; 1、扛不住&#xff0c;初创团队一般最好一人撑一快工作&#xff0c;刚毕业经验比较薄的小孩在这方面一是心理上不敢担当&#xff0c;二是能力上确实还需要磨炼成长 2、初创团队的那个环境可能…

vba执行linux命令,从VBA中的shell命令捕获输出值?

慕盖茨4494581根据Andrew Lessard的回答&#xff0c;这是一个运行命令并将输出作为字符串返回的函数 -Public Function ShellRun(sCmd As String) As StringRun a shell command, returning the output as a stringDim oShell As ObjectSet oShell CreateObject("WScript…

溢出和剪裁,可见性

内容溢出和剪裁 如果一个元素的内容对于元素大小来说过大&#xff0c;就有可能溢出元素本身。对于此情况&#xff0c;有一些解决办法可选。 溢出 overflow 值 visible(默认):内容在元素框外可见。一般会导致内容超出其自己的元素框&#xff0c;但不会改变框的形状scroll:溢出部…

C#= 栈模仿堆的操作

//原理&#xff0c;利用两个栈&#xff0c;互相作用&#xff0c;来模仿堆的效果&#xff0c;先进先出。。 1 using System;2 using System.Collections.Generic;3 using System.Linq;4 using System.Threading.Tasks;5 6 namespace TwoStacksQueue7 {8 public class Progra…

linux计划任务执行日志,linux中centos制定计划任务执行命令并且输出日志

1.写脚本最简单的 写如下代码#!/bin/shABC1.每个命令之间用;隔开说明&#xff1a;各命令的执行给果&#xff0c;不会影响其它命令的执行。换句话说&#xff0c;各个命令都会执行&#xff0c;但不保证每个命令都执行成功。2.每个命令之间用&&隔开说明&#xff1a;若前面…

Java-大集合拆分为指定大小的小集合

因为Oracle数据的in 最大允许1000 ,超过就会报错&#xff0c; 所以需要将集合拆分为多个集合进行处理. /*** 拆分集合* param <T>* param resList 要拆分的集合* param count 每个集合的元素个数* return 返回拆分后的各个集合*/public static <T> List<L…

AsyncTask与多任务

问题由来&#xff1a; 之前看到一篇博文&#xff0c;说AsyncTask不适合运行多任务&#xff0c; 多个任务不会异步执行&#xff0c; 当时只是印象里记住了一下也不确定&#xff0c; 今天把代码看了看&#xff0c; 把原因写出来。 问题的代码演示&#xff1a; 1 public class Asy…

iptables简单应用

可以修改/etc/rc.d/boot.local让规则重启后也能生效&#xff0c;如&#xff1a;/sbin/iptables -F/sbin/iptables -A INPUT -i eth0 -p tcp --sport 80 -j ACCEPT/sbin/iptables -A INPUT -i eth0 -p tcp -j DROP/sbin/iptables -A INPUT -i eth0 -p udp -j DROPiptables是一个…

linux中内部命令有哪些,linux内部命令有哪些

linux中常见的内部命令有&#xff1a;1.exit命令&#xff0c;退出当前的shell&#xff1b;2.history命令&#xff0c;显示历史执行过的命令&#xff1b;3.cd命令&#xff0c;切换当前工作目录&#xff1b;4.source命令&#xff0c;重新执行刚修改的初始化文件&#xff1b;5.ech…

使用SALT-API进入集成开发的简单样例

测试的时候&#xff0c;可以CURL -K&#xff0c;但真正作集成的时候&#xff0c;却是不可以的。 必须&#xff0c;不可以让TOKEN满天飞吧。 现在进入这个阶段了。写个样例先&#xff1a; import salt import salt.auth import salt.log import saltapiopts salt.client.LocalC…

POJ 2778

题意&#xff1a;很Uva项链题目类似。 区别&#xff1a; 1、字符串很多&#xff0c;用map hash超时&#xff0c;用Trie查找。 2、DFS判断连通&#xff0c;和并查集判连通&#xff0c;被我写错的地方时&#xff0c;查森林的时候&#xff0c;还是要Find_Set。 1 #include <ios…

linux挂载VMFS硬盘,ESX4.1挂载NFS共享存储(VMkernel)

要使用vmotion,iscsi,nfs功能&#xff0c;必须启用VMkernel端口&#xff0c;ESX 4.1默认不启用&#xff0c;ESXi 5.x默认启用。在 vCenter Server“SZVCENTER01”上调用对象“datastoreSystem-44”的“HostDatastoreSystem.CreateNasDatastore” 失败。挂载NFS存储的ESX控制台命…

Perl学习之四:语句(续)

循环控制&#xff1a;1.last 退出标签的语句块2.next 3.redo不推荐&#xff0c;循环次数不可控 4.goto不推荐。***************************************标签&#xff1a; 先定义一个 labellast|next|redo|goto label&#xff1b; last VS next 相当于C语言中的&#xff1a;las…

2017年8个最流行的Web编程趋势

互联网一直在不断的发展&#xff0c;这意味着开发人员必须及时了解当前的所有变化。人们在新闻、社交、购物到银行等各大方面都与互联网有着千丝万缕的联系。因此&#xff0c;为了满足全球数百万网络用户的需求&#xff0c;Web开发需求正在上升。Web编程趋势是在W开发的过程中不…

linux 分卷压缩到指定目录,运用在android下Linux分卷压缩与分卷解压的命令

protected static Vector execRootCmd(String paramString) {Vector localVector new Vector();try {Process localProcess Runtime.getRuntime().exec("su ");// 经过Root处理的android系统即有su命令OutputStream localOutputStream localProcess.getOutputStre…

gRPC-rs:从 C 到 Rust

介绍 在上篇文章中&#xff0c;我们讲到 TiKV 为了支持 [gRPC]&#xff0c;我们造了个轮子 [gRPC-rs]&#xff0c;这篇文章简要地介绍一下这个库。首先我们来聊聊什么是 gRPC。gRPC 是 Google 推出的基于 [HTTP2] 的开源 RPC 框架&#xff0c;希望通过它使得各种微服务之间拥有…

红帽linux无法进入tty,linux自启脚本(以及无法进入tty控制台)

1.建立需开机运行的脚本auto(可以不要后面的.sh后缀)2.放在/etc/init.d/目录下 (操作系统复制命令&#xff0c;在当前文件夹下复制sudo cp auto /etc/init.d)[可能先要对init.d取得x权限]3.赋予权限&#xff0c;在init.d文件目录下sudo chmod 775 ./auto4.执行&#xff0c;命…