解读reselect

reselect使用文档

为什么使用reselect?

说来话长,一切要从redux说起,redux在每一次dispatch之后都会让注册的回调都执行一遍,然后就是connect函数的锅了,connect实际上就是一个高阶组件,来看看connect的简单实现

    export const connect = (mapStateToProps) => (WrappedComponent) => {
        class Connect extends Component {
            static contextTypes = {
                store: PropTypes.object
            }

            constructor () {
                super()
                this.state = { allProps: {} }
            }

            componentWillMount () {
                const { store } = this.context
                this._updateProps()
                store.subscribe(() => this._updateProps())  // 这里可以发现connect函数返回的这个高阶组件帮我们在redux的store里面注册了一个函数,而这个函数的作用就是获取新的state和props,然后触发一次setState,这就必然会导致这个高阶组件的重新render,如果子组件不是继承自PureComponent或做过其他处理,那么子组件也必然会重新render,即使可能该组件涉及到的state和props都没有发生变化,这样一来就产生了性能问题,其实这个问题还好解决,通过继承PureComponent或者自己在shouldCOmponentUpdate里面做判断即可解决。但是另一个不可避免的性能问题在于mapStateToProps函数的执行,如果前端管理的数据十分复杂,每次dispatch以后所有用到store的组件都要计算mapStateToProps自然就会浪费性能,解决这个问题的方法改造mapStateToProps的入参函数,在入参函数里面缓存一个旧值,然后每次执行mapStateToProps的时候就利用新值和旧值缓存的一个浅比较来判断是否返回原值,如果浅比较相同就直接返回原值,这样就不用再做计算,节省了性能。这样对于性能的提高往往是很大的,因为一次dispatch一般只改变很少的内容。
            }

            _updateProps () {
                const { store } = this.context
                let stateProps = mapStateToProps(store.getState(), this.props) // 额外传入 props,让获取数据更加灵活方便
                this.setState({
                    allProps: { // 整合普通的 props 和从 state 生成的 props
                    ...stateProps,
                    ...this.props
                    }
                })
            }

            render () {
                return <WrappedComponent {...this.state.allProps} />
            }
        }

        return Connect;
    }

于是乎,reselect就出现了,来看看reselect的源码,注释里面写了解读:

    function defaultEqualityCheck(a, b) {
        return a === b
    }

    function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
        if (prev === null || next === null || prev.length !== next.length) {
            return false
        }

        // Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible.
        const length = prev.length
        for (let i = 0; i < length; i++) {
            if (!equalityCheck(prev[i], next[i])) {
                return false
            }
        }

        return true
    }

    export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
        let lastArgs = null
        let lastResult = null
        // we reference arguments instead of spreading them for performance reasons
        return function () {
            if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
                // apply arguments instead of spreading for performance.
                lastResult = func.apply(null, arguments)  
                // defaultMemoize被调用了两次,一次是执行函数返回一个selector,每次dispatch之后传入selector的参数是state和props,而state总是会发生变化的,所以前面的判断总是会进入到这里
                // 第二次调用时,这里的arguments是dependency函数的运算结果,而前面的判断就是看这些运算结果是否发生了变化,如果依赖项没有发生变化,及直接返回旧值
            }

            lastArgs = arguments
            return lastResult
        }
    }

    function getDependencies(funcs) {
        const dependencies = Array.isArray(funcs[0]) ? funcs[0] : funcs

        if (!dependencies.every(dep => typeof dep === 'function')) {
            const dependencyTypes = dependencies.map(
                dep => typeof dep
            ).join(', ')
            throw new Error(
                'Selector creators expect all input-selectors to be functions, ' +
                `instead received the following types: [${dependencyTypes}]`
            )
        }

        return dependencies
    }

    export function createSelectorCreator(memoize, ...memoizeOptions) {
        // 返回的这个函数就是最后导出的createSelector,memorize是传入的defaultMemorize函数
        // createSelector的入参是dependency函数和一个获取最终数据的函数
        // dependency函数可以放在一个数组里面也可以直接传入
        return (...funcs) => {
            let recomputations = 0
            const resultFunc = funcs.pop()  // pop出来的就是最后获取数据的函数
            const dependencies = getDependencies(funcs)  // 获取dependency数组

            const memoizedResultFunc = memoize(
                function () {
                    recomputations++
                    // apply arguments instead of spreading for performance.
                    return resultFunc.apply(null, arguments)
                },
                ...memoizeOptions
            )

            // If a selector is called with the exact same arguments we don't need to traverse our dependencies again.
            const selector = memoize(function () {
                const params = []
                const length = dependencies.length

                for (let i = 0; i < length; i++) {
                    // apply arguments instead of spreading and mutate a local list of params for performance.
                    params.push(dependencies[i].apply(null, arguments))
                }

                // apply arguments instead of spreading for performance.
                return memoizedResultFunc.apply(null, params) 
                // params数组存放着dependency函数的运算结果,被当做arguments传入memoizedResultFunc,其实每次dispatch之后都会触发dependency函数的重新计算,至于控制性能的问题是在memoizedResultFunc里面实现的
            })

            selector.resultFunc = resultFunc
            selector.dependencies = dependencies
            selector.recomputations = () => recomputations
            selector.resetRecomputations = () => recomputations = 0
            return selector
        }
    }

    export const createSelector = createSelectorCreator(defaultMemoize)
    // 这里export的createSelector函数就是我们所使用的函数

    export function createStructuredSelector(selectors, selectorCreator = createSelector) {
        if (typeof selectors !== 'object') {
            throw new Error(
                'createStructuredSelector expects first argument to be an object ' +
                `where each property is a selector, instead received a ${typeof selectors}`
            )
        }
        const objectKeys = Object.keys(selectors)
        return selectorCreator(
            objectKeys.map(key => selectors[key]),
                (...values) => {
                return values.reduce((composition, value, index) => {
                    composition[objectKeys[index]] = value
                    return composition
                }, {})
            }
        )
    }

总结

  1. 关于为什么redux每次dispatch一个action之后总是返回一个新的state?

    • 如果总是修改原来的state,则可能无法触发新的渲染
    • 可以实现回滚
  2. reselect的出现就是为了避免一些不必要的mapStateToProps的计算,提升性能

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容