Elementor$e.store全局状态管理器详解:基于 Redux Toolkit 的 Slices 注册、状态消费与 React 集成
【免费下载链接】elementorThe most advanced frontend drag & drop page builder. Create high-end, pixel perfect websites at record speeds. Any theme, any page, any design.项目地址: https://gitcode.com/GitHub_Trending/el/elementor
在 Elementor 编辑器前端架构中,$e.store是挂载在全局 API 对象$e上的全局状态管理器,它封装了 Redux Toolkit(内部使用 Slices 模式),让各个组件无需相互引用即可共享全局状态。本文基于仓库中的 API 文档(store.md)完整梳理$e.store的全部方法、Slices 定义约定与消费方式,并结合当前仓库中的源码实现佐证其底层工作机制,读完你可以掌握在 Elementor 组件中定义、监听和更新全局状态,以及将其接入 React 应用(Provider/useSelector/useDispatch)的完整方案。
一、$e.store是什么:全局状态管理器及其定位
$e.store的官方定义是:一个允许为组件创建全局状态(global states)的状态管理器,本质上是 Redux Toolkit 的封装,底层使用 Slices 组织状态。文档中标注其位置为core/common/assets/js/api/core/store.js。
在当前仓库中,全局$e对象的装配入口可以在 common.js 中看到:ElementorCommonApp在initComponents()中执行this.api = window.$e;,说明$e是编辑器前端的全局 API 命名空间,而$e.store正是该命名空间下的状态管理子模块(与$e.components、$e.run等并列)。
说明:本文以文档描述的方法集为权威 API 契约;关于底层单例实例、动作缓冲队列等机制细节,则来自仓库中同源的 Redux Toolkit 封装库源码(见第五节),两者设计一脉相承。
二、方法清单(完整 API 表)
文档为$e.store定义了 8 个方法,这是该 API 的完整契约:
| Method | Params | Returns | Description |
|---|---|---|---|
$e.store.register() | {String}sliceId,{Slice}instance | {void} | 注册一个新的 Redux Toolkit Slice |
$e.store.getAll() | {Object} | 以扁平数组形式获取所有已存在的 Slice ID | |
$e.store.get() | {String}sliceId | {Slice} | 按 ID 获取指定 Slice 实例 |
$e.store.getAllSlices() | {Object} | 获取全部 Slices | |
$e.store.getReduxStore() | {Object} | 获取真正的 Redux Store 对象(主要用于在 React 中配置<Provider />) | |
$e.store.getState() | {any} | 获取当前全局状态树 | |
$e.store.subscribe() | {Function}listener | {function} | 订阅状态变化,返回取消订阅函数 |
$e.store.dispatch() | {Object}action | {Object} | 分发一个 Redux action |
几个使用要点:
register()与get()以sliceId为键:注册时传入的 Slice ID 与后续get()查询的 ID 必须一致,这是 Slice 全局寻址的核心;subscribe()的返回值就是取消订阅函数(返回{function}),调用它即可停止监听,避免内存泄漏;getReduxStore()是面向 React 的出口:非 React 场景直接用getState()/subscribe(),React 场景则取原生 Store 交给<Provider />。
三、约定与规范:Slice 与组件的归属关系
文档在 "Guidelines & Conventions" 一节给出了三条硬性约定,这是正确使用$e.store的关键:
- 每个 Slice 由一个组件(component)拥有——状态定义权归属明确的组件,避免多个模块随意往全局状态塞字段;
- 组件可以覆写
defaultStates()方法,返回一组 Slices 配置(即 Redux ToolkitcreateSlice所需的initialState与reducers); - Slice 以定义对象中的键(key)标识,并自动加上组件命名空间前缀以避免冲突。
也就是说,一个组件声明若干 Slice 后,其在全局状态树中的完整路径为组件命名空间/键名。下面结合文档中的完整示例看这一约定如何落地。
四、示例一:在组件中定义全局状态(defaultStates())
文档给出的组件代码片段展示了getNamespace()与defaultStates()的配合:
// Component code.... getNamespace() { return 'example'; } defaultStates() { return { // A corresponding `Slice` instance will be available under `$e.store.get( 'example' )`. '': { initialState: { name: 'Elementor', }, reducers: { setName: ( prev, { payload } ) => { return { ...prev, name: payload, }; }, }, }, // A corresponding `Slice` instance will be available under `$e.store.get( 'example/counter' )`. counter: { initialState: { count: 0, }, reducers: { // Override the previous `count` value. setCount: ( prev, { payload } ) => { return { ...prev, count: payload, }; }, // Increment `count` by the value of `payload`. increment: ( prev, { payload } ) => ( { ...prev, count: prev.count + payload, } ), // Decrement `count` by the value of `payload`. decrement: ( prev, { payload } ) => ( { ...prev, count: prev.count - payload, } ), }, }, }; } // More component code....解读这段定义:
- 组件的
getNamespace()返回'example',因此defaultStates()返回对象中的两个键映射出两条全局状态路径:空字符串键''对应根级命名空间,可通过$e.store.get( 'example' )取得;counter键则拼出$e.store.get( 'example/counter' )。这解释了第三节“命名空间前缀 + 键名”的寻址规则; - 每个条目的
initialState是该 Slice 的初始状态树,reducers中每个函数都是一个 reducer:接收前一个状态prev与 action 的{ payload },返回新的状态对象(这里采用不可变更新写法); - 一个组件可以声明任意多个 Slice,各自独立寻址、互不干扰,这是“组件拥有 Slice”约定的直接体现。
五、示例二:消费全局状态——subscribe()+dispatch()
理解定义后,消费端同样直接。文档示例演示了“订阅变化 → 取 Slice 的 actions → 分发 action 触发 reducer → 取消订阅”的完整闭环:
const unsubscribe = $e.store.subscribe( () => { const state = $e.store.getState(); console.log( state['example/counter'].count ); } ); const { setCount } = $e.store.get( 'example/counter' ).actions; $e.store.dispatch( setCount( 10 ) ); // Will execute the `setCount` reducer. // The subscriber function will log `10`. unsubscribe(); // Will stop listening to changes.执行链路是:$e.store.get( 'example/counter' )拿到 Slice 实例,.actions即 Redux Toolkit 由reducers自动生成的 action creator 集合;setCount( 10 )产出一个携带payload: 10的 action;$e.store.dispatch()将其派发到全局 Redux Store,对应 reducer 执行后状态树更新,subscribe()注册的 listener 随即被触发,此时getState()['example/counter'].count即为10。subscribe()返回的函数最后调用unsubscribe(),监听即告停止。
从仓库源码结构看(@elementor/store库,见第五节),dispatch在 Store 实例尚未创建时会先把 action 压入pendingActions队列、待createStore时统一补发——这意味着注册 Slice 与创建 Store 的时序差不会丢失早期动作,上面的消费代码可以放心地在初始化早期调用。
六、示例三:在 React 应用中使用(Provider / useSelector / useDispatch)
$e.store可以直接像普通 Redux Store 一样接入 React 生态。文档给出的完整示例如下:
import { Provider, useDispatch, useSelector } from 'react-redux'; function App( props ) { return ( <Provider store={ $e.store.getReduxStore() }> <Counter name="Counter 1" /> <Counter name="Counter 2" /> </Provider> ); } function Counter( props ) { const count = useSelector( ( state ) => state['example/counter'].count ); const dispatch = useDispatch(); const { actions } = $e.store.get( 'example/counter' ); return ( <div> <h1>{ props.name }</h1> <button onClick={ () => dispatch( actions.decrement( 1 ) ) }> - </button> <span>{ count }</span> <button onClick={ () => dispatch( actions.increment( 1 ) ) }> + </button> </div> ); }要点拆解:
<Provider store={ $e.store.getReduxStore() }>:这是第二节方法表中getReduxStore()的典型用途——把$e.store背后的原生 Redux Store 交给 react-redux,两个Counter组件因此共享同一份全局状态;useSelector( ( state ) => state['example/counter'].count ):与纯 JS 场景相同的命名空间/键名路径直接映射到 Redux 状态树的属性访问;dispatch( actions.increment( 1 ) ):action creator 既可以来自$e.store.get( ... ).actions(如示例),也可以通过useDispatch()拿到统一 dispatch 后调用同一批 actions,两种方式等价;- 多个 React 组件(甚至跨应用模块)挂载同一 Store 后,任何一处的
dispatch都会让所有订阅该状态的组件重渲染,这正是“全局状态管理器”的价值所在。
七、源码纵深:Store 单例、动作缓冲与测试佐证
当前仓库中的 Store 实现(@elementor/store包)与$e.store文档描述的设计同构,可以从中看到几个关键机制:
1. 单例 Store + 幂等保护。createStore()在实例已存在时直接抛出'The store instance already exists.'(见 index.ts),保证全局状态树唯一;测试用例 store.test.tsx 专门断言了重复创建必须抛错。
2. 动作缓冲队列(pending actions)。dispatch在实例未创建时不丢弃 action,而是先入队,createStore完成后逐条补发并清空队列(index.ts)。这解释了为什么组件可以在 Store 就绪前就安全地注册 Slice 或触发更新。
3. Slice 注册的命名冲突检查。registerSlice()对同名 Slice 抛出Slice with name "..." already exists.(index.ts),与文档“以键 + 命名空间前缀避免冲突”的约定相互呼应——命名冲突在注册期即被拦截,而不是运行时出现状态串扰。对应测试见 store.test.tsx。
4. 基于 Selector 的精准订阅。除原始subscribe(listener)外,库还提供subscribeWithSelector(selector, listener)(index.ts):仅当 selector 选出的值发生引用变化时才通知监听者。测试(store.test.tsx)验证了:dispatch 无关 Slice 的 action 时监听者不会被误触发,而目标 Slice 更新时会收到新值——这为“多组件共享一个全局 Store 但互不干扰”提供了机制基础。
5. 中间件与测试隔离。默认中间件之外可通过addMiddleware追加自定义中间件(测试中演示了不执行next(action)即可拦截状态更新,见 store.test.tsx);deleteStore()则一次性清空实例、Slices、pending actions 与中间件集合,供单元测试隔离使用(index.ts)。
6. 类型辅助。库导出SliceState<S>类型映射(index.ts),可把某个 Slice 推断为{ [sliceName]: InitialState }的形式,在 React 侧useSelector中获得完整的状态类型提示,避免动态 Store 带来的类型推断失效问题(源码注释中说明了这是为动态创建 Store 做的必要妥协)。
需要注意:该库的 README 标注其仍处于开发阶段(under development),因此阅读其源码时应聚焦机制理解,而非将其视为已稳定的对外依赖。
八、小结与适用边界
回到$e.store本身,可以把整套心智模型压缩为三句话:
- 定义:组件覆写
defaultStates()返回 Slice 配置,以命名空间/键名成为全局状态树中的路径,Slice 由拥有它的组件负责; - 消费:非 React 场景用
get()取 Slice 的actions,配合dispatch()更新、subscribe()监听(返回函数用于退订);React 场景用getReduxStore()交给<Provider />,标准useSelector/useDispatch即可无缝工作; - 机制:底层是 Redux Toolkit——单一 Store 实例、
combineReducers组合各 Slice、注册期命名冲突检查、动作缓冲队列保证初始化时序安全。
适用边界提示:本文 API 契约以 store.md 的文档描述为准(文档标注的实现文件core/common/assets/js/api/core/store.js在当前仓库快照中未以该路径出现,$e全局对象则由 common.js 装配);文档中引用的组件约定页面(./components.md)在当前仓库中亦不存在,涉及组件注册细节时建议以packages/packages/core下各编辑器包的现有代码为准。
【免费下载链接】elementorThe most advanced frontend drag & drop page builder. Create high-end, pixel perfect websites at record speeds. Any theme, any page, any design.项目地址: https://gitcode.com/GitHub_Trending/el/elementor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考