TL;DR;
React
、Redux
的应用中,使用Reselect
来做性能优化是常见的优化手段Reselect
默认只有一份cache
,cache
命中的策略是浅比较,引用改变了会导致cache
失效Reselect
支持定制memeoize
函数和cache
命中策略在开发React
前端应用的时候,我们经常会使用Redux
做状态管理,但是随着前端代码的复杂度的上升,会引入Reselect
作为性能优化的手段之一,这篇文章就Reselect的一些应用场景作一些介绍。
在介绍Reselect
的之前,我们首先要知道Selector
这个概念,那么什么是Selector
呢?
Selector
这个概念并不是Javascrip
t或者React
、Redux
的一个概念。想象一下你去便利店买可乐,你给店员说要可口可可,这时候店员就去给你拿一罐可口可乐,这时候,店员其实就是充当了Selector
的角色,店员知道如何从各种商品里拿到你要的可口可乐,具体来说Selctor
有以下特点:
Selector
知道从哪里,以及如何去获取数据的子集Selector
会返回数据的子集
用代码来表示就是:
const getCoke = (state) => state.shop.items.coke
简单来说Selector
就是从一个大的State
上去获取子数据的函数,其实就是从一个大的Object
,根据指定的path
去取数据,如果自己不写Selector
,也可以在用的地方从state
上去取,但是这样存在的一个问题是,如果有多个地方都需要同一份数据,你不得不在多个地方加同样的代码,如果State
的结构变了,那么这样就会取修改多个文件的代码,可维护性并不高,所以从可维护性的角度来说,Selector
是很有必要的。
同样随着项目复杂度的提升,我们会有很多个Selector
,针对于获取数据粒度,不同的Selector
是可以组合成更复杂的`Selector。
简单来说就是为了性能,因为Reselector
给Selector
提供了缓存的能力,避免了重复计算;在一个复杂的Redux
的Web App中,在state
发生变化的时候,会依次调用所有的connect
,从理论上来说我们只应该去刷新和变化数据相关的组件,但是可能会存在这种情况,某些connect
中要获取的数据虽然没变,但是如果每次connect每次都返回一个新的引用,那么就会导致无谓的重刷;还存在的问题是,在connect
中回去数据的逻辑是很耗时的操作,导致性能瓶颈。这两个问题字实际的开发中,是很常见的问题,Reselect
可以在一定程度上解决这些意外的问题。
我们通过一个例子来看看,来看看Reselect
是如何提升性能的。
import React, { Component } from 'react';
import { connect } from 'react-redux'
export const countReducer = (state = {}, action) => {
switch (action.type) {
case 'INCREASE_1':
return {
...state,
demo_1: {
count: state.demo_1.count + 1,
}
};
case 'INCREASE_2':
return {
...state,
demo_2: {
count: state.demo_2.count + 1,
}
}
default:
return {
...state,
demo_1: {
count: 0,
},
demo_2: {
count: 0,
}
}
}
}
export const increase_1 = () => ({
type: 'INCREASE_1',
});
export const increase_2 = () => ({
type: 'INCREASE_2',
});
const mapStateToProps1 = (state) => {
const demo1 = { ...state.demo_1 };
return {
demoData: demo1,//demoDataSelector(state, 'demo_1'),
}
}
const mapStateToProps2 = (state, props) => {
const demo2 = { ...state.demo_2 };
return {
demoData: demo2,
}
}
class CardView1 extends Component {
render() {
console.log('...render cardview1');
return (<div className="card" style={{
backgroundColor: 'red',
width: '150px',
height: '50px',
display: 'inline-block',
color: 'white',
}}>{`Cardview1 CurrentView: ${this.props.demoData.count}`}</div>);
}
}
class CardView2 extends Component {
render() {
console.log('...render cardview2');
return (<div className="card"style={{
backgroundColor: 'blue',
width: '150px',
height: '50px',
display: 'inline-block',
marginLeft: '10px',
color: 'white',
}}>{`Cardview2 CurrentView: ${this.props.demoData.count}`}</div>);
}
}
const connected = {
CardView1: connect(mapStateToProps1)(CardView1),
CardView2: connect(mapStateToProps2)(CardView2),
}
class Counter extends Component {
render() {
return (
<div id="app-container" style={{
textAlign: 'center',
}}>
<h1>
<button className="count1" onClick={() => {
this.props.dispatch(increase_1());
}}>Increase Card 1</button>
<button className="count2" onClick={() => {
this.props.dispatch(increase_2());
}}>Increase Card 2</button>
</h1>
<connected.CardView1/>
<connected.CardView2/>
</div>
);
}
}
export default connect()(Counter);
在上面的代码中,Cardview1
和CardView2
每次会从state
上取出数据,然后放到一个新的Object
上去,导致每次都会产生一个新的引用,那么这里会导致的问题是,在我们更新demo_1
的数据的时候理当只刷新Cardview1
,但是在上面代码的情况下,刷新Cardview1
的时候也会刷新Cardview2
,反之亦然;当我点击
这里存在的问题是,在mapStateToProps
中,每次都返回了一个新的引用,所以导致虽然和组件中无关的属性更新了,但是仍然刷新了;这里我们使用Reselect
将mapStateToProps
中的Selector
包一下:
const getDemoData = (state, key) => {
return state[key];
}
const demoDataSelector = createSelector(getDemoData, data => data);
const mapStateToProps1 = (state) => {
return {
demoData: demoDataSelector(state, 'demo_1'),
}
}
const mapStateToProps2 = (state, props) => {
return {
demoData: demoDataSelector(state, 'demo_2'),
}
}
这里的话使用Reselect
来解决这个问题,这里虽然这个例子不大合适,但是这里仅仅是以这个为例子来讲解Reselect
的用法。
如果一个使用了Reselect
的组件在多个地方使用,那么其实这个组件的优化是无意义的,因为在生成下一个组件的实例的时候会将上一个组件的cache
冲掉,如下:
import React from 'react';
import ShopItems from './components/shopitems';
const Shop = () => {
<div>
<ShopItems category="belts" />
<ShopItems category="dresses" />
<ShopItems category="pants" />
</div>
}
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
const getItemsByCategory = (state,props) => state.items[props.category];
const getItemsSelector = createSelector(getItemsByCategory, items => items);
const ShopItems = () => {
return (this.props.items.map(item => {
<div>{item}</div>
}));
}
export default connect(getItemsSelector)(ShopItems);
如上,这里复用了ShopItems
这个组件,这个组件使用的Selector
使用了Reselect
,但是这里的优化是没什么卵用的,因为对于Selector
来说每次的参数都变化了,cache
并不会命中,所有我们需要在每一次实例化组件的时候,为每一个组件生成一份cache
,如下:
const makeGetItemsSelector = () => {
return createSelector(getItemsByCategory, items => items);
}
const makeMapStateToProps = () => {
const getItems = makeGetItemsSelector();
const mapStateToProps = (state, props) => {
return {
items: getItems(state, props),
}
}
}
// ...
export connect(makeMapStateToProps)(ShopItems);
如上,这里会为每一个组件生成一个私有的Selector
,就可以避免在复用组件的时候,cache
没有做对的问题。
考虑下面的代码:
const mySelectorA = state => state.a
const mySelectorB = state => state.b
// The result function in the following selector
// is simply building an object from the input selectors
const structuredSelector = createSelector(
mySelectorA,
mySelectorB,
mySelectorC,
(a, b, c) => ({
a,
b,
c
})
)
这里最后需要生成一个结构化的数据,这种情况下,Reselect
提供了生成结构化的数据的API
—> createStructuredSelector
。
const mySelectorA = state => state.a
const mySelectorB = state => state.b
const structuredSelector = createStructuredSelector({
x: mySelectorA,
y: mySelectorB
})
const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 }
因为默认的createSelector
的只提供了一份cache
,在很多情况下并不满足我们的需求,另外cache
的命中策略是浅比较,在一些情况下并不适用我们的使用场景,所以Reselect
提供了定制createSelector
的能力 —>createSelectorCreator(memoize, ...memoizeOptions)
。如下,我们使用lodash
生成一个一个无限cache
的createSelector
。
const mySelectorCreator = createSelectorCreator(_.memoize);
如上,我们就拥有了一个无限cache
的selectorCreator
。
兴趣遍地都是,坚持和持之以恒才是稀缺的