没接触过
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.lengthfor (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 = resultFuncselector.dependencies = dependenciesselector.recomputations = () => recomputationsselector.resetRecomputations = () => recomputations = 0return selector}
}
看完以上代码,带着疑问来看看 memoize 是何方神圣
defaultMemoize
在 createSelector 中使用的是默认的 defaultMemoize 。
// func 即要调用的函数,equalityCheck 默认使用提到的全等判断 defaultEqualityCheck
export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {// 前一次参数let lastArgs = null// 前一次执行结果let lastResult = null// 借助闭包缓存前一次执行的参数与结果,仍返回一个函数return function () {// 如果前后两次参数不一样,则执行 func,否则返回之前的执行结果 lastResultif (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {// apply arguments instead of spreading for performance.lastResult = func.apply(null, arguments)}lastArgs = argumentsreturn 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 将给运行时内存带来损耗,用户需要权衡决定最佳实践。
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:




文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取
Reselect是一个用于创建记忆化的选择器库,常与Redux配合使用,通过缓存计算结果来减少不必要的更新,提高React项目的性能。本文介绍了Reselect的基本原理,包括它的memoize函数和createSelectorAPI,探讨了如何自定义比较函数以优化性能,并指出在实际项目中处理变量查询型selector的挑战。最后,提到了结合高效缓存策略以平衡性能和内存占用的重要性。
1065

被折叠的 条评论
为什么被折叠?



