在提到reselect之前,我们先看下面这个情况。
import React, { Component } from 'react'
class Demo extends Component {
render() {
const { a, b, c, uabc } = this.props
return (
<div>
<h6>{a}</h6>
<h6>{b}</h6>
<h6>{c}</h6>
<h6>{uabc}</h6>
</div>
)
}
}
function u(x, y, z) {
return 6*x + 9*y + 13*z
}
Demo组件收到的props:a, b, c, u(a, b, c)。关于 u(a, b, c)的计算,我们应该放在哪里?
将计算放在redux中
store的结构如下:
store = {
a:1,
b:1,
c:1,
uabc: 28 // 6a + 9b + 13c
}
将计算u(a, b, c)放在reducer中,计算u(a, b, c)的代码部分如下:
switch(action.type) {
case changeA: {
return {
...state,
a: action.a,
uabc: u(action.a, state.b, state.c)
}
}
case changeB: {
...
}
}
这样我们的reducer 函数非常复杂了, 我们每更新一个状态值。 都得维护与这个值相关的值, 不然就会有数据不一致。
为了保证数据流清晰,我们只把最基本的状态存储在redux。那么我们需要将u(a, b, c)的计算放在组件中。
将计算放在组件中
可以将计算放在render中:
class Demo extends Component {
render() {
const { a, b, c } = this.props
const uabc = u(a, b, c)
return (
<div>
<h6>{a}</h6>
<h6>{b}</h6>
<h6>{c}</h6>
<h6>{uabc}</h6>
</div>
)
}
}
这种情况,当组件自身属性ownProps,或者setState时,都会进行计算,浪费性能。另外也违背了保持组件逻辑简单原则。
将计算放在mapStateToProps中
class Demo extends Component {
render() {
const { a, b, c, uabc } = this.props
return (
<div>
<h6>{a}</h6>
<h6>{b}</h6>
<h6>{c}</h6>
<h6>{uabc}</h6>
</div>
)
}
}
function mapStateToProps(state) {
const {a, b, c} = state
return {
a,
b,
c,
uabc: u(a, b, c)
}
}
Demo= connect(mapStateToProps)(Demo)
这种方式,组件只是接收数据展示即可。但是当store中的状态改变时,会通知所有被connect且没被销毁的组件。
如果页面上有三个组件,这三个组件存在redux上的任意状态的改变,都会触发计算u(a, b, c)。
但这通常不是问题,因为我们一般每个页面只会有一个容器组件和redux进行关联,其他子组件都是通过props的方式获取数据。当react-router切换路由时,是会销毁前一个路由的组件。同一时间只会有一个容器组件。
不过另一种情况会导致无效计算u(a, b, c):
如果Demo组件还有另一个状态属性d,也存在redux。这个属性就是一个简单的值,只用来展示。可当d变化时,也会触发u(a, b, c)的计算。不管将计算放在render还是mapStateToProps,都是无法避免的。
精准控制计算
经过上面的讨论,我们得出了一些结论:
-
redux只存基本状态
-
一个路由尽量单容器组件
但是日常开发中仍然会有类似属性d,会导致类似u(a, b, c)的无效计算。我们需要告诉程序:
只有a, b, c变化时,才会计算u(a, b, c)。大概思路:
每次计算u(a, b, c)时,都会利用闭包原理缓存a, b, c 以及 计算的结果 result。当下一次调用这个函数时,会比对新旧入参a, b, c ,如果一致则直接返回之前缓存的结果 result,无需计算。大致代码如下:
let memoizeState = null
function mapStateToProps(state) {
const {a, b, c} = state
// 如果没有缓存则直接计算u(a, b, c)
if (!memoizeState) {
memoizeState = {
a,
b,
c,
uabc: u(a, b, c)
}
} else {
// 如果a, b, c中任意一个变化,都会重新计算u(a, b, c)
if (!(a === memoizeState.a && b === memoizeState.b && c === memoizeState.c) ) {
memoizeState.uabc = u(a, b, c)
}
memoizeState.a = a
memoizeState.b = b
memoizeState.c = c
}
return memoizeState
}
神器登场 - reselect
reselect 就是用来解决这个问题的,大致用法如下:
import { createSelector } from 'reselect'
const aSelector = state => state.a
const bSelector = state => state.b
const cSelector = state => state.c
const uSelector = createSelector(
[ aSelector, bSelector, cSelector ],
(a, b, c) => u(a, b, c)
)
function mapStateToProps(state) {
const { a, b, c } = state
return {
a,
b,
c,
uabc: uSelector(state)
}
}
mapStateToProps也被叫做selector, Reselect 提供 createSelector 函数来创建可记忆的 selector。createSelector 接收一个 input-selectors 数组和一个转换函数作为参数。
当 state tree 的改变会引起 input-selector 值变化,那么 selector 会调用转换函数,传入 input-selectors 作为参数,并返回结果。如果 input-selectors 的值和前一次的一样,它将会直接返回前一次计算的数据,而不会再调用一次转换函数。这样就可以避免不必要的计算,为性能带来提升。
结论
selector(即mapStateToProps)在store发生变化的时候就会被调用,而不管是不是selector关心的数据发生改变它都会被调用,所以如果selector计算量非常大,每次更新都重新计算可能会带来性能问题。Reselect能帮你省去这些没必要的重新计算。
补充:reselect记忆功能的原理
简单说,就是利用闭包(closure),比如我们把问题简单化,让createSelector就固定接受3个参数,代码差不多就是下面这样。
const createSelector = (selector1, selector2, resultSelector) => {
let selectorCache1, // selector1的结果记忆
selectorCache2, // selector2的结果记忆
resultCache; // resultSelector的结果记忆
return (state) => {
const subState1 = selector1(state);
const subState2 = selector2(state);
if (selectorCache1 === subState1 && selectorCache2 === subState2) {
return resultCache;
}
selectorCache1 = subState1;
selectorCache2 = subState2;
resultCache = resultSelector(selectorCache1, selectorCache2);
return resultCache;
};
}
用一个函数产生一个新的函数,前一个函数中的局部变量就可以作为新产生函数的“记忆体”。
另外最近正在写一个编译 Vue 代码到 React 代码的转换器,欢迎大家查阅。