之前就听闻了reselect是一个用于react性能优化的库,并且源码只有100多行。可谓短小精悍,今天来阅读一波膜拜大佬们的思想
import { createSelector } from 'reselect'
const shopItemsSelector = state => state.shop.items
const taxPercentSelector = state => state.shop.taxPercent
const subtotalSelector = createSelector(
shopItemsSelector,
items => items.reduce((acc, item) => acc + item.value, 0)
)
const taxSelector = createSelector(
subtotalSelector,
taxPercentSelector,
(subtotal, taxPercent) => subtotal * (taxPercent / 100)
)
export const totalSelector = createSelector(
subtotalSelector,
taxSelector,
(subtotal, tax) => ({ total: subtotal + tax })
)
let exampleState = {
shop: {
taxPercent: 8,
items: [
{ name: 'apple', value: 1.20 },
{ name: 'orange', value: 0.95 },
]
}
}
console.log(subtotalSelector(exampleState)) // 2.15
console.log(taxSelector(exampleState)) // 0.172
console.log(totalSelector(exampleState)) // { total: 2.322 }
官网demo如上,通过介绍可以知道,subtotalSelector
taxSelector
totalSelector
在传进去的state不变的情况下,第二次调用不会重新计算,而是会取前一次的计算结果。在涉及到大量运算的时候,例如redux中,可以避免全局state某一小部分改变而引起这边根据小部分state进行计算的重新执行。起到性能优化的作用。下面开始阅读探读部分
先说几个简单的工具函数吧
首先是默认的比较函数,代表比较方式,可以根据业务需求换的。默认是进行全等比较
/**
* 默认的比较函数,只进行一层全等比较。根据业务需要可以自己定制。
* @param {*} a
* @param {*} b
*/
function defaultEqualityCheck(a, b) {
return a === b
}
比较两个参数是否完全相等的库…见注释。记住不要为了装逼而弃用for循环
/**
* 比较前后两次的参数是否完全相等
* @param {*} equalityCheck 比较规则,默认是使用defaultEqualityCheck全等比较
* @param {*} prev 前一次的参数
* @param {*} next 这次的参数
*/
function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
if (prev === null || next === null || prev.length !== next.length) {
return false
}
// 学到了。使用for循环是因为return后不会继续for后面的,forEach和every是会继续的,所以以后不要为了装逼而抛弃for循环了
// 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++) {
// 只要遇到一个参数不想等就退出,判断返回false
if (!equalityCheck(prev[i], next[i])) {
return false
}
}
return true
}
获取依赖,就是保证inputSelectors的每一项都是函数。否则就报错
/**
* 用来保证inputSelectors的每一项都是函数,非函数就报错。然后返回每一个inputSelector组成的数组(依赖)
* @param {*} funcs
*/
function getDependencies(funcs) {
// 因为可以两种形式传入,所以处理下
// createSelector(...inputSelectors | [inputSelectors], resultFunc)
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
}
下面是默认记忆化函数defaultMemoize
。
就是说有个func函数,每次去调用它的时候,先去比较它的参数是否和上一次参数相同,相等就返回上一次结果,不等就重新计算一遍, 并把结果和参数存到lastArgs、lastResult中,这里是用到了闭包来保存的。
/**
* 默认的记忆化函数
* @param {*} func
* @param {*} equalityCheck 比较的函数,默认情况下是判断是否全等
*/
export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
let lastArgs = null
let lastResult = null
// 这里为了性能原因。使用arguments而不是rest运算符
// we reference arguments instead of spreading them for performance reasons
return function () {
if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
// 参数改变了重新计算一遍
lastResult = func.apply(null, arguments)
}
lastArgs = arguments // 记录下这次的参数值
return lastResult
}
}
demo中引入的createSelector
其实是export const createSelector = createSelectorCreator(defaultMemoize)
这样创建的
下面看看createSelectorCreator
的实现
1、memoize
是自定义的记忆化函数,默认是上面说到的defaultMemoize
。我们可以根据自己的业务需求进行定制
2、它的内部也使用了defaultMemoize
进行优化。就是对于相同的state,不需要每一次都一次调用inputSelect去一个个获取对应的值
/**
*
* @param {*} memoize
* @param {*} memoizeOptions
*/
export function createSelectorCreator(memoize, ...memoizeOptions) {
return (...funcs) => { // createSelector的本体
let recomputations = 0
const resultFunc = funcs.pop() // 最后一个。。就是那个计算函数
const dependencies = getDependencies(funcs)// funs是inputSelectors
// resultFunc经过包装。具备了记忆功能
const memoizedResultFunc = memoize(
function () {
// 这里的arguments是每一项是inputSelector后的结果
// 把inputSelector后的结果传进resultFunc
// recomputations 用了记录调用了多少次
recomputations++
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.
// reselect内部的一个优化
// 作用就是对于相同的state,不需要每一次都一次调用inputSelect去一个个获取对应的值
const selector = defaultMemoize(function () {
// arguments应该是state
const params = []
const length = dependencies.length
for (let i = 0; i < length; i++) {
// 对每一个inputSelect结合state取出相应的值
params.push(dependencies[i].apply(null, arguments))
}
//最后供memoizedResultFunc计算使用Ss s
return memoizedResultFunc.apply(null, params)
})
// 对外提供的几个方法
// 返回那个函数
selector.resultFunc = resultFunc
//返回计算次数
selector.recomputations = () => recomputations
// 用来重置
selector.resetRecomputations = () => recomputations = 0
return selector
}
}
reselect这个库还是很吊的…主要是利用了必=闭包来保存变量,比较函数前后两次的参数值,参数值不同就重新计算。参数相同就返回之前的结果