教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载本篇指南基于 jstips 仓库的 State to Props maps with memoryTip #66展开讲解在 React Redux 应用中如何借助reselect的createSelector为mapStateToProps里的状态计算函数注入“记忆”memoization让组件只在真正相关的 state 片段变化时才重新计算派生数据。读完本文你将掌握从朴素选择器到记忆化选择器的完整改造路径并理解其缓存失效的底层原理可直接迁移到自己的 Redux 容器组件中。问题背景组件频繁更新映射函数反复重算当你用 Redux 管理状态一段时间后通常会遇到这样的尴尬状态树state tree设计得很精细每个组件从mapStateToProps中拿到的都是“不多不少”恰好需要的那部分数据可组件依然频繁地“背后更新”——总是在映射mapping总是在计算calculating。原因在于在标准的 Redux 数据流中只要 store 派发了任意 action 导致 state 变化所有通过connect订阅的容器组件都会重新执行mapStateToProps。而mapStateToProps内部往往包含filter、map、reduce、排序等派生计算逻辑。即使这次变化与当前组件完全无关这些计算也会被白白重复执行一遍。朴素实现visibleTodos 选择器的问题原文档以经典的“TODOs 列表 可见性过滤”为例展示了容器组件中获取可见待办visible TODOs的朴素实现const getVisibleTodos (todos, filter) { switch (filter) { case SHOW_ALL: return todos case SHOW_COMPLETED: return todos.filter(t t.completed) case SHOW_ACTIVE: return todos.filter(t !t.completed) } } const mapStateToProps (state) { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } }这个版本的痛点非常明确每次组件更新时getVisibleTodos都会被重新计算一遍。todos.filter(...)需要遍历整个数组如果待办列表很大或筛选逻辑更复杂多层过滤、聚合、排序重复计算的开销会随渲染频率线性放大。更关键的是这种重复计算往往是无意义的如果state.todos和state.visibilityFilter都没变那么getVisibleTodos的结果必然与上次相同完全可以复用上次的计算结果。改造第一步拆出可追踪的状态选择器要让计算具备“记忆”前提是先定义清楚“哪些状态片段是本次计算真正依赖的”。reselect 的做法是先写出一组原子级的选择器selector每个选择器只负责从 state 树中取出一个片段const getVisibilityFilter (state) state.visibilityFilter const getTodos (state) state.todos这两个函数就是可select的输入选择器input selectors。它们只做属性读取不做任何派生计算因此开销极小。它们的价值在于把getVisibleTodos的“输入依赖”显式声明出来——它只关心visibilityFilter和todos这两个片段。改造第二步用 createSelector 赋予函数记忆接下来引入reselect的createSelector把输入选择器与结果计算函数组合起来import {createSelector} from reselect const getVisibleTodos createSelector( [getVisibilityFilter, getTodos], (visibilityFilter, todos) { switch (visibilityFilter) { case SHOW_ALL: return todos case SHOW_COMPLETED: return todos.filter(t t.completed) case SHOW_ACTIVE: return todos.filter(t !t.completed) } } )createSelector接受两个参数第一个是输入选择器数组或单个选择器第二个是结果计算函数result function。计算函数的参数顺序与输入选择器数组一一对应第一个输入选择器的结果作为第一个参数visibilityFilter第二个输入选择器的结果作为第二个参数todos。说明原文档中SHOW_COMPLETED分支存在一处笔误todos.filtler(...)上文已修正为filter实际使用时请以修正版为准。此时getVisibleTodos的行为发生了本质变化它内部维护了一份缓存。每次调用时依次执行输入选择器取出当前state.visibilityFilter与state.todos将结果与上次缓存的值做引用比较即 Object.is 级别的引用相等性判断若所有输入值都未变化直接返回上次缓存的结果完全跳过switch/filter计算只要任一输入值发生变化才重新执行结果计算函数并用新结果刷新缓存。这就是文档所说的“给函数记忆”reselect 让getVisibleTodos自己知道它关心的 state 部分是否变化——变了才继续处理没变就直接返回上次结果。改造第三步mapStateToProps 传入完整 state由于createSelector返回的getVisibleTodos现在只接收一个state参数输入选择器会自行完成内部拆解mapStateToProps也要相应简化const mapStateToProps (state) { return { todos: getVisibleTodos(state) } }改造完成。现在每当 store 派发 action若visibilityFilter或todos引用未变 →getVisibleTodos直接返回缓存的数组引用connect层比较 props 时发现todos引用未变就不会触发该容器组件重新渲染前提是其他 mapStateToProps 结果也没变若相关片段变了 → 重新计算并返回新数组引用组件正常更新。也就是说“不涉及的部分 state 变化时不再有任何额外计算”这正是原文档给出的核心结论No extra calculations if the involved parts of the state havent changed!原理纵深为什么引用比较能成立createSelector的记忆化之所以可靠依赖于 Redux 状态管理的一条核心约定不可变更新。Reducer 每次返回的都是新的 state 对象未受影响的子树会保持原引用不变。因此修改某条 todo 的completed字段 →todos数组会生成新引用 → 触发getVisibleTodos重算只修改与列表无关的其他 state 片段如currentUser.name→todos与visibilityFilter的引用都不变 → 直接命中缓存。这正是“引用相等”能充当“内容是否变化”的代理的原因。若代码中存在原地 mutate如todos.push(...)新旧引用相同记忆化就会失效或返回陈旧结果——这也是 reselect 这类工具反过来倒逼你保持不可变数据流的原因。从本仓库其他文章也可以印证这一数据流设计思路Why you should use Object.is() in equality comparison-in-equality-comparison.md) 讨论了引用/值比较的语义差异而 reselect 的缓存命中正是建立在这种精确比较之上。进阶实践选择器组合与多参数派生createSelector最有价值的特性是可组合性记忆化选择器可以继续作为其他选择器的输入。例如在上面的 TODOs 例子上继续派生统计信息const getVisibleTodos createSelector( [getVisibilityFilter, getTodos], (visibilityFilter, todos) { /* 见上文实现 */ } ) const getCompletedTodoCount createSelector( [getVisibleTodos], (visibleTodos) visibleTodos.filter(t t.completed).length )此时getCompletedTodoCount依赖的是记忆化后的getVisibleTodos只要原始 state 片段未变getVisibleTodos命中缓存getCompletedTodoCount也不会重算。这种逐层缓存让大型应用的派生数据流变成一张“只有受影响分支才会被触发计算”的图而不是每次 dispatch 全量重跑。此外createSelector还支持为结果计算函数附加参数。当过滤条件不来自 store 而来自组件 props 时可以将 props 传入mapStateToProps的第二个参数并在结果函数中接收再通过createSelector的第二个参数传入自定义缓存大小或序列化比较函数。这些都属于 reselect 的扩展 API建议结合实际项目需要选用。与 React 性能主题的呼应记忆化选择器解决的是“派生计算重复执行”的问题而 jstips 仓库的 React 系列还有其他几篇与之互补的性能视角Check the reason make your page re-render by changed props and stateTip #74提供了一套在componentDidUpdate/useEffect中打印“哪些 props/state 变了”的追踪方案可以用来定位是否因mapStateToProps返回了新引用例如每次返回未缓存的新数组导致无谓重渲染——这与本文的记忆化改造正好是“诊断 根治”的配套组合Upping Performance by Appending/KeyingTip #75讲解 React reconciliation 与列表key的关系说明列表子组件复用而非重建对渲染性能的影响。当mapStateToProps每次都返回新数组引用时即便内容相同React 也会因引用变化重新协调子组件——记忆化选择器正是阻断这条链路的关键一环Keys in children components are importantTip #02则从子组件身份识别的角度提醒从数组中动态创建的组件必须携带稳定、唯一的key否则即使父级 props 引用优化到位子组件仍可能被错误重建。使用注意与适用边界记忆化并非万能以下几点需要注意输入必须稳定可比较输入选择器返回的应是引用稳定的值来自不可变 state 的对象、数组、基本类型。若某个输入选择器每次返回新建的对象字面量如(state) ({...state.settings})引用永远不同缓存将永远失效记忆化退化为纯开销。结果函数的返回引用SHOW_ALL分支直接返回todos原引用天然可复用而filter总会产生新数组只有输入未变时才会复用缓存。设计选择器时留意这一点能更好地配合connect的浅比较。缓存粒度reselect 默认只缓存最近一次的结果部分版本可通过配置调整。输入组合多、结果计算重的场景应把大计算拆成多层小选择器让每一层都能独立命中缓存。安装与运行环境reselect是独立于 React 与 Redux 的纯 JS 库需要单独安装如npm install reselect后通过模块系统引入jstips 仓库本身是文档型仓库关联文档及其繁体中文版均以代码示例说明用法不携带可执行工程实际落地时请结合你自己的 React Redux 项目环境验证。小结本文完整呈现了“给 state 到 props 的映射加记忆”的改造三步曲用朴素函数写getVisibleTodos→ 每次渲染都重算拆出getVisibilityFilter、getTodos两个原子选择器用createSelector组合出记忆化版本mapStateToProps只传完整state。改造后未涉及的 state 变化不再触发派生计算组件更新频率与计算开销都回归到“需要时才发生”。配合本仓库中关于 re-render 追踪、reconciliation 与 key 的系列文章你可以系统性地把 React Redux 应用的渲染性能理顺。赞分享教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载相关推荐3 步装好网盘直链下载助手8 家网盘解析直链直喂 IDM 和 Aria23 步装好网盘直链下载助手8 家网盘解析直链直喂 IDM 和 Aria2 你在网盘网页版点下载要么被限速到 KB 级要么要先过一遍广告页。 网盘直链下载前端Mitosis 组件规范深入解析 no-assign-props-to-state 规则——为什么不能把 props 直接赋值给 stateMitosis 组件规范深入解析 no assign props to state 规则——为什么不能把 props 直接赋值给 state 本篇技术指南聚焦前端代码生成开发工具低代码react-redux-typescript-guide选择器组合使用reselect创建记忆化选择器react redux typescript guide选择器组合使用reselect创建记忆化选择器 在React Redux应用开发中随着状态管理复杂度前端教程上一篇LinkSwift九大网盘直链提取与自动化下载的专业解决方案下一篇突破网盘限速7天精通直链下载的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考