redux性能优化 - reselect

1,843 阅读5分钟

在提到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 代码的转换器,欢迎大家查阅。

github.com/mcuking/vue…