Reselect 为什么可以优化 React 项目性能?

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

没接触过

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}
} 

defaultMemoizereselect 的核心方法,它借助闭包保存前一次参数与结果,通过比对前后参数的差异来决定是否需要执行原 func,达到记忆函数的目的。

自定义

如果觉得 createSelector 不能满足你的性能追求,reselect 完全支持用户通过 createSelectorCreator 使用自定义的 memoizeequalityCheck

详细的使用在官方文档中也有提到,不再展开。


以上就是对 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道面试题,并对每个问题作出了回答和解析。

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

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值