前端状态管理【免费下载链接】platformReactive State for Angular项目地址https://gitcode.com/gh_mirrors/pl/platform点击查看免费下载状态追踪是 SignalStore 实现自定义扩展功能的基石例如日志记录、状态撤销/重做undo/redo与存储同步如 localStorage 持久化。本文以 state-tracking.md 文档为主体结合ngrx/signals的 state-source.ts 源码与 state-source.spec.ts 测试用例讲解两种追踪方式的差异与取舍帮助你掌握getState与watchState的完整用法并能基于它们实现自定义 SignalStore 特性。为什么需要状态追踪SignalStore 的每一个状态切片都对应一个独立的 signal视图可以直接读取store.count()这样的状态信号。但当你需要观察整体状态的变化——例如把每次变更后的完整 state 快照记录下来时就需要一种能够感知任意状态切片变化的机制。在ngrx/signals中这个能力由 state-source.ts 中的getState与watchState两个函数提供它们都从 index.ts 对外导出统一适用于 SignalStore 与signalState两种状态源。方式一getState搭配effectgetState用于读取 SignalStore或signalState的当前状态快照。它的特殊之处在于当在一个响应式上下文如effect、computed中被调用时内部读取的每一个状态 signal 都会被自动追踪状态变化后调用方会自动重新执行。从 state-source.ts 的源码可以看出其实现原理它遍历存储在STATE_SOURCE符号上的全部状态信号逐个读取并聚合成一个新的 state 对象export function getStateState extends object( stateSource: StateSourceState ): State { const signals: Recordstring | symbol, Signalunknown stateSource[STATE_SOURCE]; return Reflect.ownKeys(stateSource[STATE_SOURCE]).reduce((state, key) { const value signals[key](); return { ...state, [key]: value }; }, {} as State); }因此getState并不是一个一次性快照工具而是能够参与到响应式追踪中的读取函数。基础示例effect 中追踪状态变化下面是一个计数器 Store通过withHooks的onInit钩子在effect内调用getState实现状态一变就打印import { effect } from angular/core; import { getState, patchState, signalStore, withHooks, withMethods, withState, } from ngrx/signals; export const CounterStore signalStore( withState({ count: 0 }), withMethods((store) ({ increment(): void { patchState(store, { count: store.count() 1 }); }, })), withHooks({ onInit(store) { effect(() { // The effect is re-executed on state change. const state getState(store); console.log(counter state, state); }); setInterval(() store.increment(), 1_000); }, }) );每隔 1 秒调用一次incrementeffect 就会带着最新的 state 重新执行。effect 的无闪烁glitch-free合并行为受effect本身 glitch-free 特性的影响如果同一个 tick 内状态被多次修改effect 只会以最终状态执行一次。例如连续两次调用increment你只会看到一次日志内容是最终值。这种异步合并对性能是友好的但对某些功能却是障碍。文档原文明确指出像状态 undo/redo 这样的功能需要记录 SignalStore 的全部状态变化而不能把同一 tick 内的多次更新合并掉。这就是watchState存在的意义。方式二watchState同步追踪每一次变化watchState允许同步地追踪 SignalStore 的状态变化。它接收两个参数第一个参数SignalStore或signalState实例第二个参数watcher 回调函数在每次状态变化后执行。默认情况下watchState必须在注入上下文injection context中调用并绑定其生命周期——当所在 injector 被销毁时watcher 自动清理。基础示例effect 与 watchState 的行为对比import { effect } from angular/core; import { getState, patchState, signalStore, watchState, withHooks, withState, } from ngrx/signals; export const CounterStore signalStore( withState({ count: 0 }), withMethods((store) ({ increment(): void { patchState(store, { count: store.count() 1 }); }, })), withHooks({ onInit(store) { watchState(store, (state) { console.log([watchState] counter state, state); }); // logs: { count: 0 }, { count: 1 }, { count: 2 } effect(() { console.log([effect] counter state, getState(store)); }); // logs: { count: 2 } store.increment(); store.increment(); }, }) );在这个例子中store.increment()被连续调用两次watchState的 watcher 会被执行3 次一次携带初始状态{ count: 0 }随后每次 increment 各一次{ count: 1 }、{ count: 2 }effect只会执行1 次且携带最终状态{ count: 2 }。也就是说watchState有两大特点同步执行状态一改变watcher 立刻运行不做同 tick 合并初始即执行注册 watcher 后会立刻以当前状态调用一次这正是上面出现{ count: 0 }的原因。源码视角watchState 是如何做到逐个通知的从 state-source.ts 可以看到watchState的实现骨架export function watchStateState extends object( stateSource: StateSourceState, watcher: StateWatcherState, config?: { injector?: Injector } ): { destroy(): void } { if (typeof ngDevMode ! undefined ngDevMode !config?.injector) { assertInInjectionContext(watchState); } const injector config?.injector ?? inject(Injector); const destroyRef injector.get(DestroyRef); addWatcher(stateSource, watcher); executeWatcher(stateSource, watcher); const destroy () removeWatcher(stateSource, watcher); destroyRef.onDestroy(destroy); return { destroy }; }关键机制对应如下注入上下文校验当没有显式传入injector时开发模式下会调用assertInInjectionContext断言脱离注入上下文调用会抛出NG0203: watchState() can only be used within an injection context错误自动清理从当前或传入的injector 中取得DestroyRef在onDestroy中自动移除 watcher注册即执行addWatcher之后立即调用executeWatcher所以第一次回调携带的是初始状态同步通知patchState在更新状态信号的末尾会调用notifyWatchers遍历并同步执行所有 watcher因此同 tick 内的多次patchState会触发多次回调不会被合并。值得注意的实现细节是executeWatcher使用了untracked包裹state-source.tsfunction executeWatcherState extends object( stateSource: StateSourceState, stateWatcher: StateWatcherState ): void { untracked(() { const state getState(stateSource); stateWatcher(state); }); }这意味着 watcher 内部读取的任何 signal不会泄漏到外层响应式上下文。对应的测试用例state-source.spec.ts专门验证了当patchState由某个effect触发时watcher 中读取其他 signal 不会让该 effect 额外重跑。手动清理调用destroywatchState返回一个包含destroy方法的对象。如果需要在 injector 销毁之前提前停止观察手动调用destroy即可import { patchState, signalStore, watchState, withHooks, witMethods, withState, } from ngrx/signals; export const CounterStore signalStore( withState({ count: 0 }), withMethods((store) ({ increment(): void { patchState(store, { count: store.count() 1 }); }, })), withHooks({ onInit(store) { const { destroy } watchState(store, console.log); setInterval(() store.increment(), 1_000); // Stop watching after 5 seconds. setTimeout(() destroy(), 5_000); }, }) );这里watchState(store, console.log)的返回值被解构出destroy在 5 秒后调用以终止观察此后状态再变化也不会触发 watcher。从源码看destroy的本质是调用removeWatcher把该 watcher 从STATE_WATCHERS一个以状态源为 key 的WeakMap中过滤掉state-source.ts。在注入上下文之外使用传入injectorwatchState也可以在注入上下文之外使用方法是把injector作为第三个参数config 对象传入此时生命周期绑定到该 injectorimport { Component, inject, Injector, OnInit } from angular/core; import { watchState } from ngrx/signals; import { CounterStore } from ./counter-store; Component({ /* ... */ providers: [CounterStore], }) export class Counter implements OnInit { readonly #injector inject(Injector); readonly store inject(CounterStore); ngOnInit(): void { watchState(this.store, console.log, { injector: this.#injector, }); setInterval(() this.store.increment(), 2_000); } }在这个组件示例中watcher 的生命周期与组件的 injector 绑定——组件销毁时 watcher 自动清理无需手动调用destroy。源码中config?.injector ?? inject(Injector)state-source.ts正是这条分支的实现。测试用例佐证三种清理路径都被覆盖state-source.spec.ts 为watchState提供了一套完整的验证可以直接作为你理解其行为的参考场景测试要点位置初始即执行注册后立即收到初始状态0随后patchState三次各收到1、2、3state-source.spec.tsinjector 销毁自动清理服务销毁后不再收到后续更新state-source.spec.ts手动destroy清理调用destroy后 watcher 停止state-source.spec.ts传入 injector 的清理多个 injector 各自独立销毁、互不影响state-source.spec.ts脱离注入上下文报错抛出NG0203错误state-source.spec.ts响应式上下文隔离watcher 内读取 signal 不泄漏到触发方 effectstate-source.spec.tsStore 内外均可使用既能在withHooks.onInit中使用也能在组件中注入后使用state-source.spec.ts另外测试还验证了watchState对signalState同样有效因为signalState与 SignalStore 共享同一个STATE_SOURCE机制见 signal-state.ts。两种追踪方式如何选择维度getStateeffectwatchState执行时机异步、延迟到变更通知后同步、状态更新立即触发同 tick 多次变更合并为一次只取最终值每次都触发不合并初始状态需要显式读取注册后立即回调一次适用场景日志、派生副作用、UI 联动undo/redo、状态快照、存储同步生命周期由effect所在上下文管理绑定 injector 的DestroyRef或手动destroy文档开头即点明了状态追踪的典型应用方向日志记录logging、状态撤销/重做undo/redo、存储同步storage synchronization。结合watchState的同步、逐次、不合并特性你可以实现 undo/redowatcher 中把每次状态快照 push 进历史栈撤销时从栈中弹出上一个快照并用patchState恢复再配合destroy在必要时停止记录实现存储同步watcher 中把最新状态写入localStorage/sessionStorageStore 初始化时再从存储中读取并patchState实现日志/审计用getStateeffect做轻量级的变更日志即可满足大多数需求。小结状态追踪是搭建自定义 SignalStore 特性的底层能力getState负责在响应式上下文中读取整体状态并参与自动追踪watchState则提供同步、逐次、自动绑定生命周期的状态变更回调。两者由 state-source.ts 统一实现并得到了 state-source.spec.ts 的完整验证。选择哪种方式取决于你的场景能否容忍同 tick 内的状态合并——需要完整变化轨迹时watchState是明确答案。赞分享前端状态管理【免费下载链接】platformReactive State for Angular项目地址https://gitcode.com/gh_mirrors/pl/platform点击查看免费下载相关推荐NgRx SignalStore 完整实战指南用 Signals 构建可扩展的 Angular 状态管理NgRx SignalStore 完整实战指南用 Signals 构建可扩展的 Angular 状态管理 NgRx SignalStore 是 NgRx 提供前端状态管理NgRx SignalStore Events 插件实战基于事件驱动的响应式状态管理指南NgRx SignalStore Events 插件实战基于事件驱动的响应式状态管理指南 Events 插件为 NgRx SignalStore 引入了一层基前端状态管理NgRx ESLint 规则实战with-state-no-arrays-at-root-level 与 SignalStore 状态根级约束NgRx ESLint 规则实战 with state no arrays at root level 与 SignalStore 状态根级约束 导读 本文围前端状态管理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
