前端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),仅供参考
阅读完成 · 觉得有帮助?