如何使用Reselect做性能优化

TL;DR;

  • ReactRedux的应用中,使用Reselect来做性能优化是常见的优化手段
  • Reselect默认只有一份cachecache命中的策略是浅比较,引用改变了会导致cache失效
  • Reselect支持定制memeoize函数和cache命中策略

    在开发React前端应用的时候,我们经常会使用Redux做状态管理,但是随着前端代码的复杂度的上升,会引入Reselect作为性能优化的手段之一,这篇文章就Reselect的一些应用场景作一些介绍。

    什么是Selector

    在介绍Reselect的之前,我们首先要知道Selector这个概念,那么什么是Selector呢? Selector这个概念并不是Javascript或者ReactRedux的一个概念。想象一下你去便利店买可乐,你给店员说要可口可可,这时候店员就去给你拿一罐可口可乐,这时候,店员其实就是充当了Selector的角色,店员知道如何从各种商品里拿到你要的可口可乐,具体来说Selctor有以下特点:

  • Selector知道从哪里,以及如何去获取数据的子集
  • Selector会返回数据的子集 用代码来表示就是:

    const getCoke = (state) => state.shop.items.coke

    为什么我们需要Selector?

    简单来说Selector就是从一个大的State上去获取子数据的函数,其实就是从一个大的Object,根据指定的path去取数据,如果自己不写Selector,也可以在用的地方从state上去取,但是这样存在的一个问题是,如果有多个地方都需要同一份数据,你不得不在多个地方加同样的代码,如果State的结构变了,那么这样就会取修改多个文件的代码,可维护性并不高,所以从可维护性的角度来说,Selector是很有必要的。 同样随着项目复杂度的提升,我们会有很多个Selector,针对于获取数据粒度,不同的Selector是可以组合成更复杂的`Selector

    为什么要在项目中引入Reselector

    简单来说就是为了性能,因为ReselectorSelector提供了缓存的能力,避免了重复计算;在一个复杂的Redux的Web App中,在state发生变化的时候,会依次调用所有的connect,从理论上来说我们只应该去刷新和变化数据相关的组件,但是可能会存在这种情况,某些connect中要获取的数据虽然没变,但是如果每次connect每次都返回一个新的引用,那么就会导致无谓的重刷;还存在的问题是,在connect中回去数据的逻辑是很耗时的操作,导致性能瓶颈。这两个问题字实际的开发中,是很常见的问题,Reselect可以在一定程度上解决这些意外的问题。

如何使用Reselector

我们通过一个例子来看看,来看看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);

在上面的代码中,Cardview1CardView2每次会从state上取出数据,然后放到一个新的Object上去,导致每次都会产生一个新的引用,那么这里会导致的问题是,在我们更新demo_1的数据的时候理当只刷新Cardview1,但是在上面代码的情况下,刷新Cardview1的时候也会刷新Cardview2,反之亦然;当我点击 这里存在的问题是,在mapStateToProps中,每次都返回了一个新的引用,所以导致虽然和组件中无关的属性更新了,但是仍然刷新了;这里我们使用ReselectmapStateToProps中的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?

如果一个使用了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没有做对的问题。

使用Reselect生成结构化的数据

考虑下面的代码:

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

因为默认的createSelector的只提供了一份cache,在很多情况下并不满足我们的需求,另外cache的命中策略是浅比较,在一些情况下并不适用我们的使用场景,所以Reselect提供了定制createSelector的能力 —>createSelectorCreator(memoize, ...memoizeOptions)。如下,我们使用lodash生成一个一个无限cachecreateSelector

const mySelectorCreator = createSelectorCreator(_.memoize);

如上,我们就拥有了一个无限cacheselectorCreator


兴趣遍地都是,坚持和持之以恒才是稀缺的


Written by@wang yao
I explain with words and code.

GitHub