背景:
当项目越来越大,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。