本文概述
React, Redux和Immutable.js当前是最受欢迎的JavaScript库之一, 并且在前端开发方面正迅速成为开发人员的首选。在我从事的几个React和Redux项目中, 我意识到许多开始使用React的开发人员并不完全了解React, 以及如何编写有效的代码以充分利用其潜力。
在此Immutable.js教程中, 我们将使用React和Redux构建一个简单的应用程序, 并确定一些最常见的React误用以及避免它们的方法。
数据引用问题
React就是性能。它是从头开始构建的, 具有出色的性能, 仅重新渲染了DOM的最小部分即可满足新的数据更改。任何React应用程序都应该主要由小的简单(或无状态功能)组件组成。它们很容易推理, 并且大多数都可以使shouldComponentUpdate函数返回false。
shouldComponentUpdate(nextProps, nextState) {
return false;
}
在性能方面, 最重要的组件生命周期功能是shouldComponentUpdate, 如果可能的话, 它应始终返回false。这确保了该组件将永远不会重新渲染(初始渲染除外), 从而有效地使React应用感觉极快。
如果不是这种情况, 我们的目标是对旧道具/状态与新道具/状态进行廉价的相等性检查, 如果数据不变, 则跳过重新渲染。
让我们退后一步, 回顾一下JavaScript如何对不同的数据类型执行相等性检查。
对原始数据类型(如boolean, string和integer)的相等检查非常简单, 因为它们总是与它们的实际值进行比较:
1 === 1
’string’ === ’string’
true === true
另一方面, 对复杂类型(例如对象, 数组和函数)的相等性检查是完全不同的。如果两个对象具有相同的引用(指向内存中的同一对象), 则它们是相同的。
const obj1 = { prop: ’someValue’ };
const obj2 = { prop: ’someValue’ };
console.log(obj1 === obj2); // false
即使obj1和obj2看起来相同, 它们的引用也不同。由于它们是不同的, 因此在shouldComponentUpdate函数中天真地比较它们会导致不必要地重新渲染组件。
需要注意的重要一点是, 来自Redux Reducer的数据如果设置不正确, 将始终以不同的引用提供服务, 这将导致组件每次都重新呈现。
这是我们寻求避免组件重新渲染的核心问题。
处理引用
让我们举一个例子, 其中我们具有深层嵌套的对象, 我们想将其与以前的版本进行比较。我们可以递归地遍历嵌套的对象道具并比较每个道具, 但是显然那将是极其昂贵的, 这是不可能的。
剩下的只有一个解决方案, 那就是检查引用, 但是很快就会出现新的问题:
- 如果没有任何变化, 保留引用
- 如果任何嵌套对象/数组属性值发生更改, 则更改引用
如果我们想以一种美观, 干净且性能优化的方式来完成此任务, 这并非易事。 Facebook很久以前就意识到了这个问题, 并呼吁Immutable.js进行救援。
import { Map } from ‘immutable’;
// transform object into immutable map
let obj1 = Map({ prop: ’someValue’ });
const obj2 = obj1;
console.log(obj1 === obj2); // true
obj1 = obj1.set(‘prop’, ’someValue’); // set same old value
console.log(obj1 === obj2); // true | does not break reference because nothing has changed
obj1 = obj1.set(‘prop’, ’someNewValue’); // set new value
console.log(obj1 === obj2); // false | breaks reference
Immutable.js函数均未对给定数据执行直接突变。取而代之的是, 数据在内部克隆, 变异, 并且如果有任何更改, 则返回新的引用。否则, 它将返回初始引用。必须明确设置新引用, 例如obj1 = obj1.set(…);。
React, Redux和Immutable.js示例
展示这些库的强大功能的最佳方法是构建一个简单的应用程序。还有什么比待办事项应用程序简单?
为简洁起见, 在本文中, 我们将仅介绍应用程序中对这些概念至关重要的部分。该应用程序代码的整个源代码可以在GitHub上找到。
启动应用程序后, 你会注意到对console.log的调用被方便地放置在关键区域中, 以清楚地显示DOM重新呈现的数量, 这是最小的。
与其他待办事项应用程序一样, 我们希望显示待办事项列表。当用户单击待办事项时, 我们会将其标记为已完成。另外, 我们在顶部需要一个小的输入字段来添加新的待办事项, 在底部的3个过滤器上需要允许用户在以下之间进行切换:
- 所有
- 已完成
- 活性
Redux减速器
Redux应用程序中的所有数据都驻留在单个存储对象中, 我们可以将化简工具视为将存储分为几个更容易推理的较小片段的便捷方法。由于减速器也是一种功能, 因此它也可以分成更小的部分。
我们的减速器将包括2个小部分:
- 待办事项清单
- activeFilter
// reducers/todos.js
import * as types from 'constants/ActionTypes';
// we can look at List/Map as immutable representation of JS Array/Object
import { List, Map } from 'immutable';
import { combineReducers } from 'redux';
function todoList(state = List(), action) { // default state is empty List()
switch (action.type) {
case types.ADD_TODO:
return state.push(Map({ // Every switch/case must always return either immutable
id: action.id, // or primitive (like in activeFilter) state data
text: action.text, // We let Immutable decide if data has changed or not
isCompleted: false, }));
// other cases...
default:
return state;
}
}
function activeFilter(state = 'all', action) {
switch (action.type) {
case types.CHANGE_FILTER:
return action.filter; // This is primitive data so there’s no need to worry
default:
return state;
}
}
// combineReducers combines reducers into a single object
// it lets us create any number or combination of reducers to fit our case
export default combineReducers({
activeFilter, todoList, });
与Redux连接
现在, 我们已经使用Immutable.js数据设置了Redux reducer, 让我们将其与React组件连接以传递数据。
// components/App.js
import { connect } from 'react-redux';
// ….component code
const mapStateToProps = state => ({
activeFilter: state.todos.activeFilter, todoList: state.todos.todoList, });
export default connect(mapStateToProps)(App);
在理想情况下, 连接仅应在顶级路线组件上执行, 在mapStateToProps中提取数据, 其余是将子代道具传递给孩子的基本React。在大型应用程序中, 很难跟踪所有连接, 因此我们希望将它们保持在最低限度。
请务必注意, state.todos是从Redux的CombineReducers函数返回的常规JavaScript对象(todos是reducer的名称), 但是state.todos.todoList是不可变列表, 因此将其保持在这样的状态至关重要。表单, 直到它通过了shouldComponentUpdate检查。
避免组件重新渲染
在深入研究之前, 重要的是要了解必须为组件提供哪种类型的数据:
- 任何原始类型
- 对象/数组仅以不可变形式
拥有这些类型的数据使我们能够对React组件中的道具进行浅浅的比较。
下一个示例显示了如何以最简单的方式区分道具:
$ npm install react-pure-render
import shallowEqual from 'react-pure-render/shallowEqual';
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}
函数shallowEqual将仅检查道具/状态差异深1级。它的运行速度非常快, 并且与我们的不可变数据具有完美的协同作用。必须在每个组件中编写此shouldComponentUpdate会很不方便, 但是幸运的是有一个简单的解决方案。
将shouldComponentUpdate提取到一个特殊的单独组件中:
// components/PureComponent.js
import React from 'react';
import shallowEqual from 'react-pure-render/shallowEqual';
export default class PureComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}
}
然后, 仅扩展需要该shouldComponentUpdate逻辑的任何组件:
// components/Todo.js
export default class Todo extends PureComponent {
// Component code
}
在大多数情况下, 这是一种非常干净有效的避免组件重新呈现的方法, 以后, 如果应用变得更加复杂并且突然需要自定义解决方案, 则可以轻松进行更改。
在传递函数作为道具时使用PureComponent时会出现一个小问题。由于具有ES6类的React不会自动将此绑定到功能上, 因此我们必须手动执行。我们可以通过执行以下任一操作来实现:
- 使用ES6箭头功能绑定:<Component onClick = {()=> this.handleClick()} />
- 使用bind:<Component onClick = {this.handleClick.bind(this)} />
这两种方法都会导致Component重新呈现, 因为每次都将不同的引用传递给onClick。
要变通解决此问题, 我们可以像这样在构造函数方法中预绑定函数:
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
}
// Then simply pass the function
render() {
return <Component onClick={this.handleClick} />
}
如果你多数时候发现自己预先绑定了多个功能, 我们可以导出并重用小型辅助功能:
// utils/bind-functions.js
export default function bindFunctions(functions) {
functions.forEach(f => this[f] = this[f].bind(this));
}
// some component
constructor() {
super();
bindFunctions.call(this, ['handleClick']); // Second argument is array of function names
}
如果所有解决方案都不适合你, 则始终可以手动编写shouldComponentUpdate条件。
处理组件内部的不可变数据
在当前的不可变数据设置下, 避免了重新渲染, 而组件的props内部则保留了不可变数据。有很多使用不可变数据的方法, 但是最常见的错误是使用不可变的toJS函数将数据立即转换为纯JS。
使用toJS将不可变数据深度转换为普通JS会避免避免重新渲染的整个目的, 因为这是预期的, 它非常慢, 因此应避免。那么我们如何处理不可变数据呢?
它需要按原样使用, 因此Immutable API提供了各种各样的功能, 映射并成为React组件中最常用的功能。来自Redux Reducer的todoList数据结构是不可变形式的对象数组, 每个对象代表一个单独的todo项:
[{
id: 1, text: 'todo1', isCompleted: false, }, {
id: 2, text: 'todo2', isCompleted: false, }]
Immutable.js API与常规JavaScript非常相似, 因此我们将像其他对象数组一样使用todoList。地图功能在大多数情况下证明是最好的。
在地图回调中, 我们得到todo, 它仍然是不可变形式的对象, 我们可以安全地将其传递给Todo组件。
// components/TodoList.js
render() {
return (
// ….
{todoList.map(todo => {
return (
<Todo key={todo.get('id')}
todo={todo}/>
);
})}
// ….
);
}
如果计划对不可变数据执行多个链式迭代, 例如:
myMap.filter(somePred).sort(someComp)
…然后, 非常重要的是首先使用toSeq将其转换为Seq, 并在迭代后将其返回为所需的形式, 例如:
myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()
由于Immutable.js从未直接更改给定的数据, 因此它总是需要制作另一个副本, 因此执行多次迭代可能会非常昂贵。 Seq是数据的惰性不变序列, 这意味着它将跳过跳过中间副本的创建而执行尽可能少的操作来完成其任务。 Seq被构建为以这种方式使用。
在Todo组件内部, 使用get或getIn获取道具。
很简单吧?
好吧, 我意识到, 在很多情况下, 拥有大量的get()尤其是getIn()时, 它变得非常难以理解。因此, 我决定在性能和可读性之间找到一个最佳结合点, 经过一些简单的实验, 我发现Immutable.js的toObject和toArray函数可以很好地工作。
这些函数将Immutable.js对象/数组(深度为1级)浅转换为纯JavaScript对象/数组。如果我们有任何深入嵌套的数据, 它们将保持不变的形式, 准备传递给子组件, 这正是我们所需要的。
它比get()慢了一点点, 但看起来却干净得多:
// components/Todo.js
render() {
const { id, text, isCompleted } = this.props.todo.toObject();
// …..
}
让我们一起来看看
如果你尚未从GitHub克隆代码, 那么现在是进行此操作的好时机:
git clone https://github.com/rogic89/ToDo-react-redux-immutable.git
cd ToDo-react-redux-immutable
启动服务器非常简单(确保已安装Node.js和NPM), 如下所示:
npm install
npm start
在你的Web浏览器中导航到http:// localhost:3000。在开发者控制台打开的情况下, 在添加一些待办事项的同时查看日志, 将其标记为已完成并更改过滤器:
- 加5所有项目
- 将过滤器从”全部”更改为”有效”, 然后再返回至”全部”
- 没有待办事项重新渲染, 只需过滤更改
- 将2个待办事项标记为已完成
- 重新绘制了两个待办事项, 但一次只渲染一个
- 将过滤器从”全部”更改为”有效”, 然后再返回至”全部”
- 只有2个已完成的待办事项已安装/未安装
- 活动的未重新渲染
- 从列表中间删除一个待办事项
- 仅待删除的待办事项受到影响, 其他未重新渲染
本文总结
正确使用React, Redux和Immutable.js的协同作用, 可以为大型Web应用程序中经常遇到的许多性能问题提供一些优雅的解决方案。
Immutable.js允许我们检测JavaScript对象/数组中的更改, 而无需诉诸深度相等检查的低效率, 这反过来又使React可以避免不需要时进行昂贵的重新渲染操作。这意味着Immutable.js在大多数情况下的性能往往都不错。
我希望你喜欢这篇文章, 并发现它对你将来的项目中构建React创新解决方案很有用。
相关:React组件如何使UI测试变得容易
评论前必须登录!
注册