mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-24 12:33:30 +08:00
Merge pull request #133 from GeWuYou/feat/state-dynamic-registration
feat(state): 添加运行时临时注册与注销功能
This commit is contained in:
commit
b912e6aa4d
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Scriban" Version="6.6.0" />
|
<PackageReference Include="Scriban" Version="6.6.0" />
|
||||||
|
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj"/>
|
||||||
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
||||||
<ProjectReference Include="..\GFramework.SourceGenerators.Abstractions\GFramework.SourceGenerators.Abstractions.csproj"/>
|
<ProjectReference Include="..\GFramework.SourceGenerators.Abstractions\GFramework.SourceGenerators.Abstractions.csproj"/>
|
||||||
<ProjectReference Include="..\GFramework.SourceGenerators.Common\GFramework.SourceGenerators.Common.csproj"/>
|
<ProjectReference Include="..\GFramework.SourceGenerators.Common\GFramework.SourceGenerators.Common.csproj"/>
|
||||||
|
|||||||
@ -18,4 +18,9 @@ global using System.Threading;
|
|||||||
global using System.Threading.Tasks;
|
global using System.Threading.Tasks;
|
||||||
global using NUnit.Framework;
|
global using NUnit.Framework;
|
||||||
global using NUnit.Compatibility;
|
global using NUnit.Compatibility;
|
||||||
global using GFramework.Core.Systems;
|
global using GFramework.Core.Systems;
|
||||||
|
global using GFramework.Core.Abstractions.StateManagement;
|
||||||
|
global using GFramework.Core.Extensions;
|
||||||
|
global using GFramework.Core.Property;
|
||||||
|
global using GFramework.Core.StateManagement;
|
||||||
|
global using GFramework.Core.Abstractions.Property;
|
||||||
@ -1,9 +1,3 @@
|
|||||||
using GFramework.Core.Abstractions.Property;
|
|
||||||
using GFramework.Core.Abstractions.StateManagement;
|
|
||||||
using GFramework.Core.Extensions;
|
|
||||||
using GFramework.Core.Property;
|
|
||||||
using GFramework.Core.StateManagement;
|
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.StateManagement;
|
namespace GFramework.Core.Tests.StateManagement;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -259,6 +253,137 @@ public class StoreTests
|
|||||||
Assert.That(store.LastDispatchRecord.NextState.Count, Is.EqualTo(2));
|
Assert.That(store.LastDispatchRecord.NextState.Count, Is.EqualTo(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试 reducer 句柄注销后,后续同类型 action 不会再命中该 reducer。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void RegisterReducerHandle_UnRegister_Should_Stop_Future_Reductions()
|
||||||
|
{
|
||||||
|
var store = new Store<CounterState>(new CounterState(0, "Player"));
|
||||||
|
var reducerHandle = store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
||||||
|
state with { Count = state.Count + action.Amount });
|
||||||
|
|
||||||
|
store.Dispatch(new IncrementAction(2));
|
||||||
|
reducerHandle.UnRegister();
|
||||||
|
store.Dispatch(new IncrementAction(2));
|
||||||
|
|
||||||
|
Assert.That(store.State.Count, Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试 middleware 句柄注销后,后续 dispatch 不会再经过该中间件。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void RegisterMiddleware_UnRegister_Should_Stop_Future_Pipeline_Execution()
|
||||||
|
{
|
||||||
|
var store = CreateStore();
|
||||||
|
var logs = new List<string>();
|
||||||
|
var middlewareHandle = store.RegisterMiddleware(new RecordingMiddleware(logs, "dynamic"));
|
||||||
|
|
||||||
|
store.Dispatch(new IncrementAction(1));
|
||||||
|
middlewareHandle.UnRegister();
|
||||||
|
store.Dispatch(new IncrementAction(1));
|
||||||
|
|
||||||
|
Assert.That(store.State.Count, Is.EqualTo(2));
|
||||||
|
Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试移除同一 action 类型中的某个 reducer 后,其余 reducer 仍保持原有注册顺序。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void RegisterReducerHandle_UnRegister_Should_Preserve_Remaining_Order()
|
||||||
|
{
|
||||||
|
var executionOrder = new List<string>();
|
||||||
|
var store = new Store<CounterState>(new CounterState(0, "Player"));
|
||||||
|
|
||||||
|
store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
||||||
|
{
|
||||||
|
executionOrder.Add("first");
|
||||||
|
return state with { Count = state.Count + action.Amount };
|
||||||
|
});
|
||||||
|
|
||||||
|
var middleReducer = store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
||||||
|
{
|
||||||
|
executionOrder.Add("middle");
|
||||||
|
return state with { Count = state.Count + action.Amount * 10 };
|
||||||
|
});
|
||||||
|
|
||||||
|
store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
||||||
|
{
|
||||||
|
executionOrder.Add("last");
|
||||||
|
return state with { Count = state.Count + action.Amount * 100 };
|
||||||
|
});
|
||||||
|
|
||||||
|
middleReducer.UnRegister();
|
||||||
|
store.Dispatch(new IncrementAction(1));
|
||||||
|
|
||||||
|
Assert.That(executionOrder, Is.EqualTo(new[] { "first", "last" }));
|
||||||
|
Assert.That(store.State.Count, Is.EqualTo(101));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试注册句柄的注销操作是幂等的,多次调用不会抛异常或影响其他注册项。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void RegisterHandles_UnRegister_Should_Be_Idempotent()
|
||||||
|
{
|
||||||
|
var logs = new List<string>();
|
||||||
|
var store = new Store<CounterState>(new CounterState(0, "Player"));
|
||||||
|
var reducerHandle = store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
||||||
|
state with { Count = state.Count + action.Amount });
|
||||||
|
var middlewareHandle = store.RegisterMiddleware(new RecordingMiddleware(logs, "dynamic"));
|
||||||
|
|
||||||
|
Assert.That(() =>
|
||||||
|
{
|
||||||
|
reducerHandle.UnRegister();
|
||||||
|
reducerHandle.UnRegister();
|
||||||
|
middlewareHandle.UnRegister();
|
||||||
|
middlewareHandle.UnRegister();
|
||||||
|
}, Throws.Nothing);
|
||||||
|
|
||||||
|
store.Dispatch(new IncrementAction(1));
|
||||||
|
|
||||||
|
Assert.That(store.State.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(logs, Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试 dispatch 进行中注销 reducer 和 middleware 时,
|
||||||
|
/// 当前 dispatch 仍使用开始时的快照,而后续 dispatch 会看到注销结果。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void UnRegister_During_Dispatch_Should_Affect_Next_Dispatch_But_Not_Current_One()
|
||||||
|
{
|
||||||
|
using var entered = new ManualResetEventSlim(false);
|
||||||
|
using var release = new ManualResetEventSlim(false);
|
||||||
|
|
||||||
|
var logs = new List<string>();
|
||||||
|
var store = new Store<CounterState>(new CounterState(0, "Player"));
|
||||||
|
var reducerHandle = store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
||||||
|
state with { Count = state.Count + action.Amount });
|
||||||
|
var blockingHandle = store.RegisterMiddleware(new BlockingMiddleware(entered, release));
|
||||||
|
var recordingHandle = store.RegisterMiddleware(new RecordingMiddleware(logs, "dynamic"));
|
||||||
|
|
||||||
|
var dispatchTask = Task.Run(() => store.Dispatch(new IncrementAction(1)));
|
||||||
|
|
||||||
|
Assert.That(entered.Wait(TimeSpan.FromSeconds(2)), Is.True, "middleware 未按预期进入阻塞阶段");
|
||||||
|
|
||||||
|
reducerHandle.UnRegister();
|
||||||
|
blockingHandle.UnRegister();
|
||||||
|
recordingHandle.UnRegister();
|
||||||
|
release.Set();
|
||||||
|
|
||||||
|
Assert.That(dispatchTask.Wait(TimeSpan.FromSeconds(2)), Is.True, "dispatch 未在释放 middleware 后完成");
|
||||||
|
Assert.That(store.State.Count, Is.EqualTo(1), "当前 dispatch 应继续使用启动时抓取的 reducer 快照");
|
||||||
|
Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" }));
|
||||||
|
|
||||||
|
store.Dispatch(new IncrementAction(1));
|
||||||
|
|
||||||
|
Assert.That(store.State.Count, Is.EqualTo(1), "后续 dispatch 应看到 reducer 已被注销");
|
||||||
|
Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" }));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。
|
/// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -33,16 +33,18 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 已注册的中间件链,按添加顺序执行。
|
/// 已注册的中间件链,按添加顺序执行。
|
||||||
|
/// 每个条目都持有稳定身份,便于通过注销句柄精确移除而不影响其他同类中间件。
|
||||||
/// Dispatch 开始时会抓取快照,因此运行中的分发不会受到后续注册变化影响。
|
/// Dispatch 开始时会抓取快照,因此运行中的分发不会受到后续注册变化影响。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly List<IStoreMiddleware<TState>> _middlewares = [];
|
private readonly List<MiddlewareRegistration> _middlewares = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按 action 具体运行时类型组织的 reducer 注册表。
|
/// 按 action 具体运行时类型组织的 reducer 注册表。
|
||||||
/// Store 采用精确类型匹配策略,保证 reducer 执行顺序和行为保持确定性。
|
/// Store 采用精确类型匹配策略,保证 reducer 执行顺序和行为保持确定性。
|
||||||
|
/// 每个 reducer 通过注册条目获得稳定身份,以支持运行时精确注销。
|
||||||
/// Dispatch 开始时会抓取对应 action 类型的 reducer 快照。
|
/// Dispatch 开始时会抓取对应 action 类型的 reducer 快照。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Dictionary<Type, List<IStoreReducerAdapter>> _reducers = [];
|
private readonly Dictionary<Type, List<ReducerRegistration>> _reducers = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 已缓存的局部状态选择视图。
|
/// 已缓存的局部状态选择视图。
|
||||||
@ -92,34 +94,6 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
_stateComparer = comparer ?? EqualityComparer<TState>.Default;
|
_stateComparer = comparer ?? EqualityComparer<TState>.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取最近一次分发的 action 类型。
|
|
||||||
/// </summary>
|
|
||||||
public Type? LastActionType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return _lastActionType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取最近一次真正改变状态的时间戳。
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeOffset? LastStateChangedAt
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return _lastStateChangedAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前状态快照。
|
/// 获取当前状态快照。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -163,9 +137,7 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
enteredDispatchScope = true;
|
enteredDispatchScope = true;
|
||||||
context = new StoreDispatchContext<TState>(action!, _state);
|
context = new StoreDispatchContext<TState>(action!, _state);
|
||||||
stateComparerSnapshot = _stateComparer;
|
stateComparerSnapshot = _stateComparer;
|
||||||
middlewaresSnapshot = _middlewares.Count > 0
|
middlewaresSnapshot = CreateMiddlewareSnapshot();
|
||||||
? _middlewares.ToArray()
|
|
||||||
: Array.Empty<IStoreMiddleware<TState>>();
|
|
||||||
reducersSnapshot = CreateReducerSnapshot(context.ActionType);
|
reducersSnapshot = CreateReducerSnapshot(context.ActionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,34 +184,6 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前订阅者数量。
|
|
||||||
/// </summary>
|
|
||||||
public int SubscriberCount
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return _listeners.Count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取最近一次分发记录。
|
|
||||||
/// </summary>
|
|
||||||
public StoreDispatchRecord<TState>? LastDispatchRecord
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return _lastDispatchRecord;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 订阅状态变化通知。
|
/// 订阅状态变化通知。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -341,6 +285,62 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取最近一次分发的 action 类型。
|
||||||
|
/// </summary>
|
||||||
|
public Type? LastActionType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _lastActionType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取最近一次真正改变状态的时间戳。
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset? LastStateChangedAt
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _lastStateChangedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前订阅者数量。
|
||||||
|
/// </summary>
|
||||||
|
public int SubscriberCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _listeners.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取最近一次分发记录。
|
||||||
|
/// </summary>
|
||||||
|
public StoreDispatchRecord<TState>? LastDispatchRecord
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _lastDispatchRecord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建一个用于当前状态类型的 Store 构建器。
|
/// 创建一个用于当前状态类型的 Store 构建器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -353,6 +353,7 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册一个强类型 reducer。
|
/// 注册一个强类型 reducer。
|
||||||
/// 同一 action 类型可注册多个 reducer,它们会按照注册顺序依次归约状态。
|
/// 同一 action 类型可注册多个 reducer,它们会按照注册顺序依次归约状态。
|
||||||
|
/// 该重载保留现有链式配置体验;若需要在运行时注销,请改用 <see cref="RegisterReducerHandle{TAction}(IReducer{TState, TAction})"/>。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
||||||
/// <param name="reducer">要注册的 reducer 实例。</param>
|
/// <param name="reducer">要注册的 reducer 实例。</param>
|
||||||
@ -360,25 +361,13 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
/// <exception cref="ArgumentNullException">当 <paramref name="reducer"/> 为 <see langword="null"/> 时抛出。</exception>
|
/// <exception cref="ArgumentNullException">当 <paramref name="reducer"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
public Store<TState> RegisterReducer<TAction>(IReducer<TState, TAction> reducer)
|
public Store<TState> RegisterReducer<TAction>(IReducer<TState, TAction> reducer)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(reducer);
|
RegisterReducerHandle(reducer);
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
var actionType = typeof(TAction);
|
|
||||||
if (!_reducers.TryGetValue(actionType, out var reducers))
|
|
||||||
{
|
|
||||||
reducers = [];
|
|
||||||
_reducers[actionType] = reducers;
|
|
||||||
}
|
|
||||||
|
|
||||||
reducers.Add(new ReducerAdapter<TAction>(reducer));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 使用委托快速注册一个 reducer。
|
/// 使用委托快速注册一个 reducer。
|
||||||
|
/// 该重载保留现有链式配置体验;若需要在运行时注销,请改用 <see cref="RegisterReducerHandle{TAction}(Func{TState, TAction, TState})"/>。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
||||||
/// <param name="reducer">执行归约的委托。</param>
|
/// <param name="reducer">执行归约的委托。</param>
|
||||||
@ -390,23 +379,86 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
return RegisterReducer(new DelegateReducer<TAction>(reducer));
|
return RegisterReducer(new DelegateReducer<TAction>(reducer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个强类型 reducer,并返回可用于注销该 reducer 的句柄。
|
||||||
|
/// 该句柄只会移除当前这次注册,不会影响同一 action 类型下的其他 reducer。
|
||||||
|
/// 若在 dispatch 进行中调用注销,当前这次 dispatch 仍会使用开始时抓取的 reducer 快照,
|
||||||
|
/// 注销仅影响之后的新 dispatch。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
||||||
|
/// <param name="reducer">要注册的 reducer 实例。</param>
|
||||||
|
/// <returns>用于注销当前 reducer 注册的句柄。</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="reducer"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
|
public IUnRegister RegisterReducerHandle<TAction>(IReducer<TState, TAction> reducer)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(reducer);
|
||||||
|
|
||||||
|
var actionType = typeof(TAction);
|
||||||
|
var registration = new ReducerRegistration(new ReducerAdapter<TAction>(reducer));
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (!_reducers.TryGetValue(actionType, out var reducers))
|
||||||
|
{
|
||||||
|
reducers = [];
|
||||||
|
_reducers[actionType] = reducers;
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers.Add(registration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DefaultUnRegister(() => UnRegisterReducer(actionType, registration));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用委托快速注册一个 reducer,并返回可用于注销该 reducer 的句柄。
|
||||||
|
/// 适合测试代码或按场景临时挂载的状态逻辑。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
||||||
|
/// <param name="reducer">执行归约的委托。</param>
|
||||||
|
/// <returns>用于注销当前 reducer 注册的句柄。</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="reducer"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
|
public IUnRegister RegisterReducerHandle<TAction>(Func<TState, TAction, TState> reducer)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(reducer);
|
||||||
|
return RegisterReducerHandle(new DelegateReducer<TAction>(reducer));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 添加一个 Store 中间件。
|
/// 添加一个 Store 中间件。
|
||||||
/// 中间件按添加顺序包裹 reducer 执行,可用于日志、审计或调试。
|
/// 中间件按添加顺序包裹 reducer 执行,可用于日志、审计或调试。
|
||||||
|
/// 该重载保留现有链式配置体验;若需要在运行时注销,请改用 <see cref="RegisterMiddleware"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="middleware">要添加的中间件实例。</param>
|
/// <param name="middleware">要添加的中间件实例。</param>
|
||||||
/// <returns>当前 Store 实例,便于链式配置。</returns>
|
/// <returns>当前 Store 实例,便于链式配置。</returns>
|
||||||
/// <exception cref="ArgumentNullException">当 <paramref name="middleware"/> 为 <see langword="null"/> 时抛出。</exception>
|
/// <exception cref="ArgumentNullException">当 <paramref name="middleware"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
public Store<TState> UseMiddleware(IStoreMiddleware<TState> middleware)
|
public Store<TState> UseMiddleware(IStoreMiddleware<TState> middleware)
|
||||||
|
{
|
||||||
|
RegisterMiddleware(middleware);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个 Store 中间件,并返回可用于注销该中间件的句柄。
|
||||||
|
/// 中间件按注册顺序包裹 reducer 执行;注销只会移除当前这次注册。
|
||||||
|
/// 若在 dispatch 进行中调用注销,当前这次 dispatch 仍会使用开始时抓取的中间件快照,
|
||||||
|
/// 注销仅影响之后的新 dispatch。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="middleware">要注册的中间件实例。</param>
|
||||||
|
/// <returns>用于注销当前中间件注册的句柄。</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="middleware"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
|
public IUnRegister RegisterMiddleware(IStoreMiddleware<TState> middleware)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(middleware);
|
ArgumentNullException.ThrowIfNull(middleware);
|
||||||
|
|
||||||
|
var registration = new MiddlewareRegistration(middleware);
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_middlewares.Add(middleware);
|
_middlewares.Add(registration);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return new DefaultUnRegister(() => UnRegisterMiddleware(registration));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -563,20 +615,54 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
return activeListeners.Count > 0 ? activeListeners.ToArray() : Array.Empty<Action<TState>>();
|
return activeListeners.Count > 0 ? activeListeners.ToArray() : Array.Empty<Action<TState>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为当前中间件链创建快照。
|
||||||
|
/// 该方法自行获取状态锁,避免调用方必须记住“只能在已加锁条件下调用”这一隐含约束,
|
||||||
|
/// 从而降低未来重构时把它误用到锁外路径中的风险。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>当前中间件链的快照;若未注册则返回空数组。</returns>
|
||||||
|
private IStoreMiddleware<TState>[] CreateMiddlewareSnapshot()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_middlewares.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<IStoreMiddleware<TState>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = new IStoreMiddleware<TState>[_middlewares.Count];
|
||||||
|
for (var i = 0; i < _middlewares.Count; i++)
|
||||||
|
{
|
||||||
|
snapshot[i] = _middlewares[i].Middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为当前 action 类型创建 reducer 快照。
|
/// 为当前 action 类型创建 reducer 快照。
|
||||||
/// Dispatch 在离开状态锁前复制列表,以便后续在锁外执行稳定、不可变的 reducer 序列。
|
/// 该方法自行获取状态锁,避免让快照安全性依赖调用方的锁顺序知识。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="actionType">当前分发的 action 类型。</param>
|
/// <param name="actionType">当前分发的 action 类型。</param>
|
||||||
/// <returns>对应 action 类型的 reducer 快照;若未注册则返回空数组。</returns>
|
/// <returns>对应 action 类型的 reducer 快照;若未注册则返回空数组。</returns>
|
||||||
private IStoreReducerAdapter[] CreateReducerSnapshot(Type actionType)
|
private IStoreReducerAdapter[] CreateReducerSnapshot(Type actionType)
|
||||||
{
|
{
|
||||||
if (!_reducers.TryGetValue(actionType, out var reducers) || reducers.Count == 0)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
return Array.Empty<IStoreReducerAdapter>();
|
if (!_reducers.TryGetValue(actionType, out var reducers) || reducers.Count == 0)
|
||||||
}
|
{
|
||||||
|
return Array.Empty<IStoreReducerAdapter>();
|
||||||
|
}
|
||||||
|
|
||||||
return reducers.ToArray();
|
var snapshot = new IStoreReducerAdapter[reducers.Count];
|
||||||
|
for (var i = 0; i < reducers.Count; i++)
|
||||||
|
{
|
||||||
|
snapshot[i] = reducers[i].Adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -592,6 +678,42 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注销一个中间件注册条目。
|
||||||
|
/// 仅精确移除与当前句柄关联的条目,避免误删同一实例的其他重复注册。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registration">要移除的中间件注册条目。</param>
|
||||||
|
private void UnRegisterMiddleware(MiddlewareRegistration registration)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_middlewares.Remove(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注销一个 reducer 注册条目。
|
||||||
|
/// 若该 action 类型下已无其他 reducer,则同时清理空注册桶,保持注册表紧凑。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="actionType">reducer 对应的 action 类型。</param>
|
||||||
|
/// <param name="registration">要移除的 reducer 注册条目。</param>
|
||||||
|
private void UnRegisterReducer(Type actionType, ReducerRegistration registration)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (!_reducers.TryGetValue(actionType, out var reducers))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers.Remove(registration);
|
||||||
|
if (reducers.Count == 0)
|
||||||
|
{
|
||||||
|
_reducers.Remove(actionType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 适配不同 action 类型 reducer 的内部统一接口。
|
/// 适配不同 action 类型 reducer 的内部统一接口。
|
||||||
/// Store 通过该接口在运行时按 action 具体类型执行 reducer,而不暴露内部装配细节。
|
/// Store 通过该接口在运行时按 action 具体类型执行 reducer,而不暴露内部装配细节。
|
||||||
@ -632,6 +754,18 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示一条 reducer 注册记录。
|
||||||
|
/// 该包装对象为运行时注销提供稳定身份,同时不改变 reducer 的执行顺序语义。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ReducerRegistration(IStoreReducerAdapter adapter)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取真正执行归约的内部适配器。
|
||||||
|
/// </summary>
|
||||||
|
public IStoreReducerAdapter Adapter { get; } = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 基于委托的 reducer 适配器实现,便于快速在测试和应用代码中声明 reducer。
|
/// 基于委托的 reducer 适配器实现,便于快速在测试和应用代码中声明 reducer。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -656,6 +790,18 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示一条中间件注册记录。
|
||||||
|
/// 通过显式注册对象而不是直接存储中间件实例,可在重复注册同一实例时保持精确注销。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class MiddlewareRegistration(IStoreMiddleware<TState> middleware)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取注册的中间件实例。
|
||||||
|
/// </summary>
|
||||||
|
public IStoreMiddleware<TState> Middleware { get; } = middleware;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示一个 Store 状态监听订阅。
|
/// 表示一个 Store 状态监听订阅。
|
||||||
/// 该对象用于支持初始化回放与正式订阅之间的原子衔接,避免 SubscribeWithInitValue 漏掉状态变化。
|
/// 该对象用于支持初始化回放与正式订阅之间的原子衔接,避免 SubscribeWithInitValue 漏掉状态变化。
|
||||||
|
|||||||
@ -147,6 +147,43 @@ var store = (Store<PlayerState>)Store<PlayerState>
|
|||||||
- 测试里快速组装不同配置的 Store
|
- 测试里快速组装不同配置的 Store
|
||||||
- 不希望把 Store 的装配细节散落在多个调用点
|
- 不希望把 Store 的装配细节散落在多个调用点
|
||||||
|
|
||||||
|
## 运行时临时注册与注销
|
||||||
|
|
||||||
|
如果某个 reducer 或 middleware 只需要在一段生命周期内生效,例如调试探针、临时玩法规则、
|
||||||
|
或场景级模块扩展,可以直接使用 `Store<TState>` 提供的句柄式注册 API:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class LoggingMiddleware<TState> : IStoreMiddleware<TState>
|
||||||
|
{
|
||||||
|
public void Invoke(StoreDispatchContext<TState> context, Action next)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Dispatching: {context.ActionType.Name}");
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var store = new Store<PlayerState>(new PlayerState(100, "Player"));
|
||||||
|
|
||||||
|
var reducerHandle = store.RegisterReducerHandle<DamageAction>((state, action) =>
|
||||||
|
state with { Health = Math.Max(0, state.Health - action.Amount) });
|
||||||
|
|
||||||
|
var middlewareHandle = store.RegisterMiddleware(new LoggingMiddleware<PlayerState>());
|
||||||
|
|
||||||
|
store.Dispatch(new DamageAction(10));
|
||||||
|
|
||||||
|
reducerHandle.UnRegister();
|
||||||
|
middlewareHandle.UnRegister();
|
||||||
|
```
|
||||||
|
|
||||||
|
这里有两个重要约束:
|
||||||
|
|
||||||
|
- `RegisterReducerHandle()` 和 `RegisterMiddleware()` 返回的是当前这一次注册的精确注销句柄
|
||||||
|
- 如果在某次 `Dispatch()` 已经开始后再调用 `UnRegister()`,当前这次 dispatch 仍会继续使用开始时抓取的快照,注销只影响后续新的
|
||||||
|
dispatch
|
||||||
|
|
||||||
|
如果你只需要初始化阶段的链式配置,继续使用 `RegisterReducer()` 和 `UseMiddleware()` 即可;
|
||||||
|
如果你需要运行时清理,就使用上面的句柄式 API。
|
||||||
|
|
||||||
## 官方示例:角色面板状态
|
## 官方示例:角色面板状态
|
||||||
|
|
||||||
下面给出一个更贴近 GFramework 实战的完整示例,展示如何把 `Store<TState>` 放进 Model,
|
下面给出一个更贴近 GFramework 实战的完整示例,展示如何把 `Store<TState>` 放进 Model,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user