wp-calypso 的 createSelector 详解:用 @automattic/state-utils 构建带缓存失效机制的 Redux 记忆化选择器
前端CMS【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址https://gitcode.com/gh_mirrors/wp/wp-calypso点击查看免费下载wp-calypsoWordPress.com 的前端应用的 Redux 状态树刻意保持精简避免冗余数据存储但精简状态意味着大量数据需要在选择器中即时派生与过滤。本文基于packages/state-utils/src/create-selector/README.md及其源码实现完整讲解createSelector的三个参数、缓存失效原理、缓存键生成规则、多选择器依赖的数组简写以及如何在 wp-calypso 的代码库中落地使用这个记忆化工具。什么是记忆化选择器为什么 wp-calypso 需要它从项目状态设计的角度看wp-calypso 力求让 Redux 状态树中不存放冗余数据代价是如果选择器本身求值耗时比如要在成百上千条 post 中过滤出某一个站点的帖子同样的计算会被反复执行带来性能问题。记忆化选择器memoized selector正是为此设计的它把计算结果缓存起来当能够确认派生所依赖的那部分状态没有变化时直接跳过昂贵的派生计算返回上一次的结果。automattic/state-utils包提供的createSelector就是 wp-calypso 中实现这一模式的标准工具实现文件位于 create-selector/index.ts包的入口 index.ts 将其与extendAction、getInitialState、withStorageKey一并导出。createSelector 的函数签名与三个参数createSelector接受以下参数第一个必选后两个可选选择函数selector从 state 中选取数据的函数。它接收一个 state 对象和任意数量的其他参数计算出结果该结果会被缓存供后续复用。依赖函数getDependants返回该选择器所依赖的状态树片段的函数。它可以是单个函数也可以是一个返回状态值数组的函数此外还支持直接传入依赖选择器数组的简写形式见下文。可选缓存键函数getCacheKey自定义内部记忆化函数所用缓存键的函数。从源码可以确认完整类型定义见 index.ts#L74-L112export default function createSelector TState, TProps extends any[], TDepProps extends TProps, TDerivedState, ( selector: ( state: TState, ...props: TProps ) TDerivedState, getDependants: | Dependant TState, TDepProps, any | Dependant TState, TDepProps, any [] DEFAULT_GET_DEPENDANTS, getCacheKey: ( state: TState, ...props: TProps ) string DEFAULT_GET_CACHE_KEY ): ( state: TState, ...props: TProps ) TDerivedState两个可选参数都有默认值这一点值得注意默认依赖函数DEFAULT_GET_DEPENDANTSindex.ts#L27-L28直接返回整个state。也就是说不传第二个参数时选择器默认监视整个状态树任何顶层 state 对象的变更都会导致缓存失效。测试用例中专门验证了这一默认行为相同 state 重复调用时底层选择器只执行 1 次state 变化后再调用则执行 2 次见 test/index.js#L202-L256。默认缓存键函数DEFAULT_GET_CACHE_KEY则是把参数join()成字符串细节见下文。典型用法为站点过滤文章列表README 中的经典例子状态中包含 post 对象每个 post 归属于某个站点。要拿到某站点的全部文章就需要过滤所有已知 post——这是一次昂贵操作。用createSelector创建记忆化函数后只要state.posts不变这次昂贵计算就只做一次export const getSitePosts createSelector( ( state, siteId ) state.posts.filter( ( post ) post.site_ID siteId ), ( state ) [ state.posts ] );使用时只需关注第一个参数的函数签名——这里需要传入 state 和站点 IDconst sitePosts getSitePosts( state, siteId );只要state.posts保持不变该结果只会被计算一次。这一点有直接测试佐证相同( state, siteId )连续调用两次后expect( selector ).toHaveBeenCalledTimes( 1 )见 test/index.js#L48-L64而传入不同siteId时会产生不同的缓存键底层选择器分别计算test/index.js#L82-L113。wp-calypso 仓库中真实的选择器也是这个形态例如 client/state/posts/selectors/get-site-post.jsimport { createSelector } from automattic/state-utils; import calypso/state/posts/init; export const getSitePost createSelector( ( state, siteId, postId ) { if ( ! siteId ) { return null; } const manager state.posts.queries[ siteId ]; if ( ! manager ) { return null; } return manager.getItem( postId ); }, ( state ) state.posts.queries );这里第二个参数把依赖范围收窄到state.posts.queries而不是整个状态树——这是避免缓存频繁失效的关键写法。缓存失效机制如何知道该重新计算这是 README FAQ 中第二个问题的核心记忆化选择器如何知道何时重新计算结果因为 Redux 不鼓励直接修改 state所以可以确信只有当所关心的状态树片段与之前严格不相等时状态才算发生了变化。因此createSelector要求传入一个函数返回一个值或一组值作为本选择器所依赖的状态片段。源码中的失效逻辑非常清晰index.ts#L96-L112return Object.assign( function ( state: TState, ...args: TProps ) { let currentDependants getDependantsFn( state, ...( args as TDepProps ) ); if ( ! Array.isArray( currentDependants ) ) { currentDependants [ currentDependants ]; } if ( lastDependants ! isShallowEqual( currentDependants, lastDependants ) ) { memoizedSelector.cache.clear(); } lastDependants currentDependants; return memoizedSelector( state, ...args ); }, { memoizedSelector } );可以拆成三步求当前依赖值每次调用时执行getDependantsFn( state, ...args )如果返回值不是数组会被包装成单元素数组。浅比较判断失效用wordpress/is-shallow-equal的isShallowEqual与上一次的依赖值逐项比较。注意这是浅比较——只要依赖片段中某一项的引用变了Redux 不可变更新天然会产生新引用就认为状态已变化执行memoizedSelector.cache.clear()清空整个缓存。执行记忆化选择器缓存被清空后第一次以新依赖计算并写回缓存后续相同参数则命中缓存。依赖函数返回数组时数组中的每一项都会参与浅比较因此可以精确声明多个依赖片段。测试用例覆盖了这条链路state.posts引用变化后再次调用结果正确且底层选择器被重新执行test/index.js#L115-L155数组依赖值同样生效test/index.js#L157-L176。另一个值得留意的行为依赖函数会被传入与选择器完全相同的参数。测试should call dependant state getter with arguments验证了getDeps收到的正是( state, 1, 2, 3 )test/index.js#L272-L280。这意味着依赖函数本身也可以基于siteId之类的参数来定位要监视的状态片段。参数传递与缓存键与 reselect 的关键区别FAQ 的第三个问题能否给记忆化选择器传参数答案是肯定的而且这是 wp-calypso 选择器中非常常见的模式。README 特别指出这是它与 reselect 这类实现类似目标的主流工具的关键差异之一reselect 的输入选择器只接收 state而createSelector允许把siteId、postId这类业务参数直接穿进选择器与依赖函数。但传参有一个约束README 与源码一致地强调了它内部记忆化函数通过一次简单的Array.prototype.join调用计算缓存键因此参数不应是复杂对象。源码中默认缓存键函数分开发/生产两种形态index.ts#L36-L52const DEFAULT_GET_CACHE_KEY ( () { if ( production process.env.NODE_ENV ) { return ( _: unknown, ...args: unknown[] ) args.join(); } return ( _: unknown, ...args: unknown[] ) { const hasInvalidArg args.some( ( arg ) { return arg ! VALID_ARG_TYPES.includes( typeof arg ); } ); if ( hasInvalidArg ) { warn( Do not pass complex objects as arguments for a memoized selector ); } return args.join(); }; } )();要点缓存键由除 state 外的参数join()成字符串生成_吃掉 state...args收集其余参数。因此getSitePosts( state, 2916284 )与getSitePosts( state, 38303081 )是不同的缓存条目。生产环境下只做纯粹的join()零额外开销。开发环境下若任何参数是复杂对象typeof不在VALID_ARG_TYPES [ number, boolean, string ]index.ts#L13会通过wordpress/warning输出警告 Do not pass complex objects as arguments for a memoized selector。测试用例传入了{}、[]、( 1, [] )三个非法场景断言警告恰好被调用 3 次test/index.js#L66-L80。之所以复杂对象不可靠根源就在join()两个内容相同但引用不同的对象会拼出不同的键缓存命中就会失效而两个键相同但对象不同的极端情况则可能导致错误命中。因此约定俗成的做法是参数只传字符串、数字、布尔这类可稳定序列化的原始值wp-calypso 的选择器普遍传siteId、postId这类 ID 值正符合该约定。如果默认的join键确实不够用第三个参数允许传入自定义缓存键函数它同样接收state和全部参数。测试中的例子const getSitePostsWithCustomGetCacheKey createSelector( selector, ( state ) state.posts, ( state, siteId ) CUSTOM${ siteId } ); getSitePostsWithCustomGetCacheKey( { posts: {} }, 2916284 ); expect( getSitePostsWithCustomGetCacheKey.memoizedSelector.cache.has( CUSTOM2916284 ) ).toBe( true );见 test/index.js#L258-L270。声明对多个选择器的依赖数组简写FAQ 的第四个问题如果新选择器依赖foo、bar、baz三个状态选择器的结果怎么写标准写法是把三个选择器都放进依赖函数createSelector( ( state ) foo( state ) bar( state ), ( state ) [ foo( state ), bar( state ), baz( state ) ] );由于这是反复出现的模式源码提供了简写第二个参数直接传一个选择器数组index.ts#L60-L63 的makeSelectorFromArraycreateSelector( ( state ) foo( state ) bar( state ), [ foo, bar, baz ] );makeSelectorFromArray的实现是把数组中的每个依赖选择器 map 一遍返回它们的结果数组——const makeSelectorFromArray TState, TProps extends any[] ( dependants: ( ( state: TState, ...args: TProps ) any )[] ) ( state: TState, ...args: TProps ) dependants.map( ( dependant ) dependant( state, ...args ) );于是createSelector内部用typeof getDependants function ? getDependants : makeSelectorFromArray( getDependants )统一处理两种输入index.ts#L93-L94。两个测试用例分别验证了依赖选择器数组的缓存语义state 变化前后各计算一次共 2 次test/index.js#L178-L200以及数组中每个选择器都会被传入全部参数test/index.js#L282-L291。访问与管理内部缓存FAQ 的最后一个问题虽然很少需要这样做但可以管理内部缓存——它是以.cache属性暴露的Map挂在返回函数的memoizedSelector属性上getSitePosts.memoizedSelector.cache; // 一个 Map getSitePosts.memoizedSelector.cache.clear(); // 手动清空测试文件把这个 API 用得很自然beforeEach中通过getSitePosts.memoizedSelector.cache.clear()隔离各用例test/index.js#L17-L20并断言typeof getSitePosts.memoizedSelector为functiontest/index.js#L22-L24。这个逃生口在写测试、或需要在某个业务事件如数据重置时强制刷新派生数据时会派上用场。底层支撑automattic/js-utils 的 memoizecreateSelector内部对选择函数做记忆化时调用的是同仓库automattic/js-utils包导出的memoizeindex.ts#L90实现在 packages/js-utils/src/memoize.tsconst memoized function ( this: unknown, ...args: Args ): Return { const key resolver ? resolver.apply( this, args ) : args[ 0 ]; const { cache } memoized; if ( cache.has( key ) ) { return cache.get( key ) as Return; } const result func.apply( this, args ); cache.set( key, result ); return result; } as MemoizedFunction Args, Return ; memoized.cache new Map unknown, Return ();这解释了createSelector的两层缓存结构如何协作内层memoize 的 Map按缓存键默认是参数join()结果存储同一组参数下的计算结果。多个siteId的值可以并存于同一 Map 中。外层createSelector 的 lastDependants 浅比较一旦依赖片段变化就调用cache.clear()把内层 Map 整体清空从而让所有参数的旧结果一次性失效。也就是说参数不同走 Map 的多键并存依赖状态变了走整表清空两者配合构成了完整的缓存策略。依赖与适用前提使用createSelector时的实际前提可以从 packages/state-utils/package.json 确认包名为automattic/state-utils描述为 A collection of Redux state utilities构建产物分 CJS/ESM 两套main指向dist/cjs/index.jsmodule指向dist/esm/index.js源码入口为src/index.ts。运行时依赖包括automattic/js-utils提供memoize、wordpress/is-shallow-equal依赖浅比较、wordpress/warning开发环境参数警告以及redux/redux-thunk。在 wp-calypso 仓库内通过 workspace 方式引用automattic/state-utils: workspace:^形态客户端代码直接import { createSelector } from automattic/state-utils如 get-site-post.js 所示。需要遵守的使用约束汇总依赖片段必须按引用比较失效——Redux 不可变更新reducer 返回新引用是这套机制生效的前提传给选择器的参数不要用复杂对象开发环境会警告生产环境则只有join()行为、无警告保护默认监视整个 state除非你有理由接受任何状态变化都失效否则建议显式传入精确的getDependants依赖函数会与选择器收到完全相同的参数可以据此把依赖范围进一步收窄到参数相关的状态片段。小结createSelector是 wp-calypso 在精简状态树 昂贵派生计算矛盾下的标准答案一个参数定义计算一个参数声明依赖支持函数或选择器数组两种形态一个可选参数定制缓存键。源码层面用参数 join 成缓存键 依赖浅比较触发整表清空两层机制保证了缓存既能在同一状态下命中复用又能在依赖变化时彻底失效。配合.memoizedSelector.cache这个可观测、可管理的Map这套工具在 create-selector 测试套件 中得到了从命中、失效、多参数到自定义缓存键的完整行为验证。赞分享前端CMS【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址https://gitcode.com/gh_mirrors/wp/wp-calypso点击查看免费下载相关推荐Redux Thunk选择器缓存记忆化策略Redux Thunk选择器缓存记忆化策略 在Redux应用开发中随着状态树的增长频繁计算派生数据可能导致性能瓶颈。选择器Selector作为从Red前端Redux Thunk选择器缓存策略何时记忆化Redux Thunk选择器缓存策略何时记忆化 你是否遇到过Redux应用性能瓶颈当应用状态频繁更新选择器Selector重复计算相同结果时界面可能前端Atmosphère 010000000000002b 致命错误完整修复指南3 个层级从存档到 prodinfoAtmosphère 010000000000002b 致命错误完整修复指南3 个层级从存档到 prodinfo 在 Atmosphère 自制固件下运行游戏前端CMS上一篇终极图片批量处理指南Umi-CUT让你的图片工作流效率翻倍下一篇Claude Subconscious跨会话连续性体验如何一句话接上昨天没写完的重构创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Playnite 主题改 3 处 XAML 就能加动画

Playnite 主题改 3 处 XAML 就能加动画

Playnite 主题改 3 处 XAML 就能加动画 【免费下载链接】Playnite Video game library manager with support for wide range of 3rd party libraries and game emulation support, providing one unified interface for your games. 项目地址: https://gitcode.com/GitHub_T…

2026/10/9 7:30:09 阅读更多 →
动态频谱与数字VU表的技术破局:腾泰技术在高通蓝牙音频平台上的显示交互创新

动态频谱与数字VU表的技术破局:腾泰技术在高通蓝牙音频平台上的显示交互创新

动态频谱与数字VU表的技术破局:腾泰技术在高通蓝牙音频平台上的显示交互创新引言在蓝牙音频设备日益同质化的今天,显示交互体验正在成为区分产品档次的核心指标之一。动态频谱图随音乐节奏律动、数字VU表实时反映音量变化——这些看似“锦上添花”的视觉…

2026/10/9 7:30:09 阅读更多 →
C++自动化构建落地:GitLab+Arbess流水线实践与踩坑指南

C++自动化构建落地:GitLab+Arbess流水线实践与踩坑指南

做C项目的自动化构建,我以前踩过的坑比写过的类还多。尤其是当代码库越来越大、依赖越来越乱、还要在几台生产主机上频繁更新二进制的时候,光靠手动打包、scp、重启服务,迟早要出事故。后来我把GitLab和Arbess这套组合梳理顺了,整…

2026/10/9 7:30:09 阅读更多 →

最新新闻

『项目管理精要』第 7 章 团队演进与冲突解决:从单打独斗到带队攻坚

『项目管理精要』第 7 章 团队演进与冲突解决:从单打独斗到带队攻坚

从一名卓越的个人贡献者(Individual Contributor, IC)成长为优秀的技术主管(TL),最大挑战在于“如何带出一支高效能的自组织团队”。在平衡矩阵或弱矩阵组织中,成员往往来自不同的职能部门,兼顾多个项目,团队容易陷入推诿扯皮或效率低下的泥潭。TL 需要理解塔克曼团队演…

2026/10/9 7:59:27 阅读更多 →
『项目管理精要』第 1 章 矩阵组织与角色解密:双重汇报环境下的协同之道

『项目管理精要』第 1 章 矩阵组织与角色解密:双重汇报环境下的协同之道

在传统职能型组织中,技术人员往往归属于固定的技术部门,按照垂直层级接收指令;而在纯项目型组织中,团队则随项目的启动而组建、随项目的收尾而解散。然而,在绝大多数中大型科技企业和软件研发团队中,最为常见的组织形态是弱矩阵组织(Weak Matrix)与平衡矩阵组织(Balan…

2026/10/9 7:59:27 阅读更多 →
苏州微观文化传媒企业宣传片服务深度评测

苏州微观文化传媒企业宣传片服务深度评测

在制造业品牌升级的浪潮中,许多企业负责人都遇到过这样的尴尬场景:花费不菲制作的企业宣传片,拿到行业展会上播放时,却因为画面质感粗糙、技术逻辑表达不清,无法打动潜在客户;或是为了 IPO 路演紧急赶制的视…

2026/10/9 7:59:27 阅读更多 →
Java框架 SpringCloud 快速入门: NacosRule 同集群优先的负载均衡

Java框架 SpringCloud 快速入门: NacosRule 同集群优先的负载均衡

概述 集群属性配好了,实例也按机房分开了,但 order-service 调 user-service 时照样跨机房——因为默认的负载均衡规则根本不认识 Nacos 的集群概念,得把规则换成 NacosRule。 纲要 承接:服务分级存储模型(服务 → …

2026/10/9 7:59:27 阅读更多 →
10.7【A】

10.7【A】

301暴力递归先求最少删除的次数但关键问题在于如何不重不漏的知道所有可能的结果考虑使用dfs即对于每个位置都尝试删除,首先要保证删除后字符串合法,其次再查询结果当中是否已经存在dfs保留,已删除的数量,当前的左括号数&#xff…

2026/10/9 7:59:27 阅读更多 →
输电线路行波测距原理与Simulink仿真实战解析

输电线路行波测距原理与Simulink仿真实战解析

干了几年输电线路故障分析,我最头疼的一件事,就是线路跳闸之后要第一时间给调度报出故障点在哪。传统测距算法靠工频量硬算,遇到高阻接地、运行方式变化大的场景,误差几公里是家常便饭,现场巡线的人沿着线路找一整夜也…

2026/10/9 7:58:26 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →