Reselect 为什么可以优化 React 项目性能?
Hi,请问有没有听说过 reselect ?
没接触过
Reselect 是一个用于创建记忆的“selector”函数的库。
通常与 Redux 一起使用,但也适用于任何普通的 JS 不可变数据场景。
- selector 可以计算衍生数据,它允许让 Redux 存储尽可能少的 state
- selector 很高效,它只有在某个参数发生变化时才会进行重新计算
- selector 是可组合的,它可以作为其他 selector 的入参
以上是来自官方的介绍,个人简单理解:
我们可以用它包装数据(如 Redux 的 state),并利用其缓存入参的能力减少不必要的更新,从而达到性能优化,一举多得。
来看个简单的用例感受一下魅力。
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 }
用过而已
如果你正在使用 reselect,并还没去深入了解过,建议打开源码(v4.0.0 非 ts 版本一共 108 行)学习一番,因为它真的小而美。
开始源码阅读之前,我们先了解两个核心工具方法。
defaultEqualityCheck
全等比较 a b 两个参数。
function defaultEqualityCheck(a, b) {
return a === b
}
(偷偷告诉你,这是源码第一行,真美!)
areArgumentsShallowlyEqual
主要目的是比较 prev 与 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
}
如上代码片段,它遍历各元素,借助 equalityCheck
进行比对,一旦不相等就直接退出。
一般我们只会用到 reselect 的 createSelector
方法,那就从它开始好了。
createSelector
学习之前,我们先看看它的函数签名
createSelector(...inputSelectors | [inputSelectors], resultFunc)
它接受若干个 selector 或一个 selector 数组以及 resultFunc
(最后一个)作为参数,其中resultFunc
接收前面 selector 计算出来的结果作为入参进行加工,并得到期望结果。
下面继续
export const createSelector = createSelectorCreator(defaultMemoize)
就一行代码:使用 defaultMemoize
作为 createSelectorCreator
的参数,并将结果导出。
看样子我们得去看看 createSelectorCreator
了。
createSelectorCreator
createSelectorCreator
接收若干个参数,返回一个接收若干个以函数为参数的方法,即 selector 。
export function createSelectorCreator(memoize, ...memoizeOptions) {
return (...funcs) => {
// 重新计算的次数
let recomputations = 0
// 使用时传入的最后一个参数
const resultFunc = funcs.pop()
/*
pop 最后一个参数后,前面参数的都是依赖,可参考 createSelector 的函数签名
如果传入的依赖不全是函数,将会抛出错误
*/
const dependencies = getDependencies(funcs)
/*
根据上文函数签名,resultFunc 接收其他 selector 参数的计算结果作为参数
并使用记忆函数缓存入参,使用 recomputations 统计重新计算次数
*/
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
// 一一计算依赖 selector 的结果,存入 params 作为 resultFunc 的入参
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)
})
// selector 上的其他属性可以在开发单测时使用
selector.resultFunc = resultFunc
selector.dependencies = dependencies
selector.recomputations = () => recomputations
selector.resetRecomputations = () => recomputations = 0
return selector
}
}
看完以上代码,带着疑问来看看 memoize 是何方神圣
defaultMemoize
在 createSelector
中使用的是默认的 defaultMemoize
。
// func 即要调用的函数,equalityCheck 默认使用提到的全等判断 defaultEqualityCheck
export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
// 前一次参数
let lastArgs = null
// 前一次执行结果
let lastResult = null
// 借助闭包缓存前一次执行的参数与结果,仍返回一个函数
return function () {
// 如果前后两次参数不一样,则执行 func,否则返回之前的执行结果 lastResult
if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
// apply arguments instead of spreading for performance.
lastResult = func.apply(null, arguments)
}
lastArgs = arguments
return lastResult
}
}
defaultMemoize
是 reselect
的核心方法,它借助闭包保存前一次参数与结果,通过比对前后参数的差异来决定是否需要执行原 func,达到记忆函数的目的。
自定义
如果觉得 createSelector
不能满足你的性能追求,reselect 完全支持用户通过 createSelectorCreator
使用自定义的 memoize
与 equalityCheck
。
详细的使用在官方文档中也有提到,不再展开。
以上就是对 reselect createSelector 的完整链路源码分析,在这份“小而美”的源码中并没有太多出奇的地方,现在回到文章题目:Reselect 为什么可以优化 React 项目性能?
我想认真看完以上分析的各位应该都能回答一二。
reselect 使用闭包保存上一次的参数 lastArgs
与结果 lastResult
,只有当依赖中的某个 Redux state 发生了变化,导致前后参数比对不一致了,才会触发 selector 的再次计算。这避免了 react 组件的不必要的更新,从而达到了性能优化的效果。
还能更优吗?
但还没完,我们会发现真正在项目使用过程中,往往会有需要使用变量查询型的 selector,如:
const conversationLastMessageSelector = (conversationId: string) => createSelector(
getMessageSelector(conversationId),
entitySelector,
(messages, entity) => {
// ...
}
)
通过上诉分析,我们知道对于 conversationLastMessageSelector
这个 selector,仅仅只会缓存输入的 conversationId
与上一次相同的结果,对于实际列表使用场景来说,缓存将不复存在。
学习 Faster JavaScript Memoization For Improved Application Performance,我们得到高效而又简单的缓存函数
function memoize (f) {
return function () {
const args = Array.prototype.slice.call(arguments)
f.memoize = f.memoize || {}
return args in f.memoize
? f.memoize[args]
: (f.memoize[args] = f.apply(this, args))
}
}
不难看出,我们可以使用高阶缓存函数 memoize
包裹 conversationLastMessageSelector
,它将实现对每个 conversationId 的记忆函数的缓存,大概如下
f.memoize = {
123: function memoize() {...},
234: ...
345: ...
}
通过 f.memoize
实现了变量查询型 selector 的缓存相比无缓存版本对 React 组件渲染有质的优化效果。但不同的 conversationId 对应的缓存函数都在挂载在 f.memoize
上,如果没有加任何缓存策略进行维护,不断增加的 conversationId 将给运行时内存带来损耗,用户需要权衡决定最佳实践。
转载自:https://juejin.cn/post/7147294478074658823