前言
我们平时在使用react-redux
时,用到最多的函数大概就是mapStateToProps
和mapDispatchToProps
。其中,
mapStateToProps
函数允许我们将 store
中的数据作为 props
绑定到组件上,它的第一个参数就是 Redux 的整个状态树(state):
const Counter = props => <div>Count: {props.count}</div>;
const mapStateToProps = state => ({ count: state.count });
export default connect(mapStateToProps)(Counter);
上述代码里,我们从state
中摘取了 count
属性。因为返回了具有 count
属性的对象,所以 Counter
组件的props
中会有一个 count
字段。
一切看起来很完美。但是仔细想一下,每次状态树更新之后,redux 都会调用一次mapStateToProps
,当我们的应用足够大,组件众多时,这里面的大量冗余计算就值得重视了。
这个时候就需要 Reselect 出马了
正文
我们先看一个小栗子:
假设我们有一组复选框,用来罗列各个商品的名称和价钱,底部用来展示我们选择商品的总价,我们用 redux 实现:
const getItems = state => state.items;
const getTotal1 = createSelector(
getItems,
items => {
console.log("total-with-reselect 将要计算");
return items.reduce(
(acc, item) => acc + (item.checked ? item.price : 0),
0
);
}
);
const getTotal2 = state => {
console.log("total-without-reselect 将要计算");
return state.items.reduce(
(acc, item) => acc + (item.checked ? item.price : 0),
0
);
};
const mapStateToProps = state => ({
total1: getTotal1(state),
total2: getTotal2(state)
});
我们通过createSelector
创建了一个记忆选择器getTotal1
,另外创建了一个不带缓存的求和函数getTotal2
。另外,我们在页面中加一个 input 输入框,并且将其值绑定到 redux 的 state 中,用作干扰项。
结果如下:
对比发现,从 state 中取值的方式有一些细微改变:getTotal1
是通过getItems
提取 state 中的源数据,然后再通过一个包装后的函数计算总和;getTotal2
则是直接从 state 中拿值并计算总和。
前者的好处是:getItems
返回值不变时,计算函数不会重新执行。也就是说,当 state 中的items
没有变化时,reselect 会直接返回上一次计算缓存的结果,避免多余的重复计算。
那么reselect
是如何实现这一机制的呢?看一下源码:
/**
* 默认的比较函数
* @param {*} a 待比较项
* @param {*} b 待比较项
*/
function defaultEqualityCheck(a, b) {
return a === b;
}
/**
* 参数是否浅等
* @param {Function} equalityCheck 比较函数
* @param {Object} prev 前一次参数
* @param {Object} next 后一次参数
*/
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;
}
/**
* 默认的记忆函数(核心)
* @param {Function} func 计算结果函数
* @param {Function} equalityCheck 比较函数
*/
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);
}
lastArgs = arguments;
return lastResult;
};
}
/**
* 获取input-selectors数组
* @param {Array} funcs input-selectors数组
*/
function getDependencies(funcs) {
// input-selectors数组可以有两种形式:
// 1.[input-selector1, input-selector2...]
// 2.[[input-selector1, input-selector2...]]
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;
}
/**
* 选择器创建器创建方法
* @param {Function} memoize 记忆函数
* @param {...any} memoizeOptions 记忆函数参数(可不传)
*/
export function createSelectorCreator(memoize, ...memoizeOptions) {
// funcs: 创建选择器传入的input-selectors和resultFunc
return (...funcs) => {
let recomputations = 0;
const resultFunc = funcs.pop();
const dependencies = getDependencies(funcs);
// 真正的记忆函数
// createSelector方法中,memoize就是defaultMemoize
const memoizedResultFunc = memoize(function() {
recomputations++;
// apply arguments instead of spreading for performance.
// 这里arguments是各个依赖项(input-selector)执行结果的数组(下文的params)
return resultFunc.apply(null, arguments);
}, ...memoizeOptions);
// 内部二次优化: 使用完全相同的参数(state)调用选择器,则不需要再次遍历依赖项(input-selectors)
// 因为reducer是不一定会返回一个新的state,所以state没变的时候,真正的记忆函数就不用被调用。
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.
// 这里arguments是state
// 根据传入的state计算每个input-selector结果, 并将结果push到params数组中
params.push(dependencies[i].apply(null, arguments));
}
// apply arguments instead of spreading for performance.
// 调用真正的记忆函数, 并返回结果
return memoizedResultFunc.apply(null, params);
});
selector.resultFunc = resultFunc;
selector.dependencies = dependencies;
selector.recomputations = () => recomputations;
selector.resetRecomputations = () => (recomputations = 0);
return selector;
};
}
// 默认的选择器创建方法
export const createSelector = createSelectorCreator(defaultMemoize);
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;
}, {});
});
}
阅读完源码发现,这个库的代码量很小,主要就是采用了闭包 + 高阶函数来实现,核心方法其实就是defaultMemoize
。
思考
由于 Reselect 缓存结果这一特性,我们可以将其延伸一下,不一定非要结合 redux 使用。
例如,给定一组坐标数据绘制图表:
const data = [
{ x: 1, y: 3 },
{ x: 2, y: 7 },
{ x: 3, y: 2 },
{ x: 4, y: 9 },
{ x: 5, y: 3 }
];
普通写法
// 假定有一个画图API:DrawAPI
const drawFn = DrawAPI.draw;
普通写法,每次后台来了新数据时,无论具体的值是否发生变化都重新绘制,势必会造成不必要的性能浪费。
进阶写法
import { defaultMemoize } from "reselect";
const checkEqualFn = (oldData, newData) => {
if (oldData.length !== newData.length) {
return false;
} else {
const length = oldData.length;
for (let i = 0; i < length; i++) {
const { x: xOld, y: yOld } = oldData[i];
const { x: xNew, y: yNew } = newData[i];
if (xOld !== xNew || yOld !== yNew) {
return false;
}
}
return true;
}
};
const drawFn = defaultMomize(DrawAPI.draw, checkEqualFn);
进阶写法,只有当后台给的数据发生变化时,才重新绘制。
参考链接
https://juejin.im/post/5ae7dac151882567113afc0d
https://segmentfault.com/a/1190000011916280