react性能优化之reselect技术要点原理分析

1,100 阅读3分钟

背景:

当项目越来越大,store很复杂,数据获取很复杂, connect里面的 mapStateToprops会在每次 dispath action的时候,都会重复执行一遍。会让cpu一直工作,使得页面卡顿。

为了有个直观的理解,下面来看个例子

function getSum(state){
    let arrs = state.sum.items;
    return arrs.reduce((pre,item)=>{
        return pre+item;
    })
}

function getMultiplication(state){
    let arrs = state.shops;
    return arrs.reduce((pre,item)=>{
        return pre*item;
    })
}
function getBalance(){
    let sums = getSum();
    let a = getMultiplication();
    return sums*a/state.count;
}


let mapStateToProps={
    balance:getBalance
}

此时页面里面 dispatch a action,只改变了count的值, 但是getSum,getMultiplication,getBalance都会重新执行一遍。

但是如果能对getSum,getMultiplication根据参数做memorize,则只需要计算最后的getBalance即可。

即对入参进行缓存,当入参不变的时候 就不用重新计算。

react推荐不可变数据,这使得出现了一种基于它的优化思路。 看过react-redux的同学应该知道,它内部做了一个优化,如果前后计算出的state不变,就不会去触发对应组件的重新render。如下:

 function dispatch(action) {
    // Swap the state
    const previousState = currentState;
    currentState = computeNextState(currentState, action);

    // Notify the observers
    const changedKeys = Object.keys(currentState).filter(key =>
      currentState[key] !== previousState[key]
    );
    emitChange(changedKeys);
  }

只是现在把相同的优化思路放到了从store里select数据的时候。

有一个库 reselect 实现了这种优化方式。

用法:

seelctor(deps:fn[],resultFn:(...rest)=>({...rest}))
let state={
 count:50,
 sum: [4,5,2,3,4,5,6,7,8,...]//有1000项
}
function getSum(state){
    let arrs = state.sum;
    return arrs.reduce((pre,item)=>{
        return pre+item;
    })
}

function getMultiplication(state){
    let arrs = state.sum;
    return arrs.reduce((pre,item)=>{
        return pre*item;
    })
}
const subtotalSelector = createSelector(
    [getSum,getMultiplication],
    function getBalance(sums,a){
         // sum ,a分别是getSum,getMultiplication的返回值
         return sums*a/state.count;
}
  )
//使用
console.log(subtotalSelector(state))
exampleState1={
    ...state.shops
}

只有当传入的参数引用不一致的时候会重新计算,比如变成了exampleState1,即该obj的引用变了。 react redux 都是推崇不可变数据,一旦要变数据,则该部分的state需要重新生成。 即引用变了。 完美契合 reselect。

原理

我们知道 lodash里面有一个 memory函数,它的实现大概是这样

function memory(fn) {
    let cacheRes = [], lastArgs = [], tempRes
    return function() {
        //传入的参数
        if (isEquel(arguments, lastArgs)) {
            return cacheRes;
        }
        cacheRes = fn.apply(null, arguments);
        lastArgs = arguments;
        return cacheRes;
    }
}

创建selector的时候,获取前面deps参数数组,依次使用上面的memory函数,对每一个依赖进行memory。返回memory后的函数。

运行的时候,mermory函数内部,先比较参数,如果有变更,执行传入的 resultFn函数,根据依赖计算对应的值,缓存在内部。没有变化,则返回之前的计算值。 所以主要的缓存功能是靠memory来实现的。

简单实现

function createSelector(...fns){
    let resultFn = fns.pop();
    let deps = fns;
    return function(){
        let res =[];
        for(let i=0;i<deps.length;i++){
            res.push(deps[i].apply(null,arguments));
        }
        return resultFn.apply(null,res);
    }
}

这样可以完成基本的功能,但是现在如果再次执行 一次调用,还会重复执行。

即参数变了需要重新计算

整体实现

function createSelector(...fns) {
    let resultFn = fns.pop();
    let deps = fns
      , count = 0;
     //传入参数变了 但是deps可能不变
    const memorizedResFn = memory(function() {
     //计算变更次数
        count++;
        return resultFn.apply(null, arguments);
    })
    //入参变了会重新计算
    let selector = memory(function() {
        let res = [];
        for (let i = 0; i < deps.length; i++) {
            res.push(deps[i].apply(null, arguments));
        }
        return memorizedResFn.apply(null, res);
    })
    selector.getCounter = ()=>count;

    return selector
}
function isEquel(pre, next) {
    if (pre.length != next.length)
        return false;
    for (let i = 0; i < next.length; i++) {
        if (pre[i] !== next[i])
            return false;
    }
    return true;
}

function memory(fn) {
    let cacheRes = [], lastArgs = [], tempRes
    return function() {
        //传入的参数
        if (isEquel(arguments, lastArgs)) {
            return cacheRes;
        }
        cacheRes = fn.apply(null, arguments);
        lastArgs = arguments;
        return cacheRes;
    }
}

注意 上面createSelector返回的selector是被memory的,可以把它当做一个普通的函数,有一个输入就有一个被memorized的输出,所以可以再次传入其他的selector作为依赖。

作为其他deps的话,输入参数也是一致的。 返回参数由最后的函数来决定

let fn1 =createSelector([(b)=>a.items],(a)=>{a})

所以就有了这种写法

createSelector([fn1dep,fn2dep],()=>{});

而且 ,作为另一个 deps必须要作为一个整体。要使用 []括起来。内部获取的时候 如果知道是个数组,则会整体去拿第一项去计算。这里是一个递归。

总结:

当store很复杂的时候,对应的selector也会很复杂,多次重复运行。我们从react-redux出发,分析了它的性能优化的精髓,reselect 这个库吸收了它的精华,用来对selector做缓存。

最后,实现了一个简单但是功能齐全的reselect。