3. 响应式
更新时间:2026-07-01 — 新增通过
SetChildProp/BindChildProp在组件之间传递 prop。
Prop<T> — 响应式属性
public class HealthBar : SusComponent
{
public Prop<float> Health = new(100f);
public Prop<string> Name = new("Player");
protected override void Created()
{
Watch(Health, (oldVal, newVal) =>
{
Debug.Log($"Health: {oldVal} → {newVal}");
});
}
private void TakeDamage(float amount)
{
Health.Value -= amount; // UI will update automatically
}
}特性:
- 隐式转换:
Prop<float>的行为与float相同(通过implicit operator) - 按值比较:如果新值等于旧值,
Changed事件不会触发 - IL2CPP-safe:不使用 reflection
Computed<T> — 计算属性
public class Inventory : SusComponent
{
public Prop<int> Gold = new(100);
public Prop<int> Gems = new(50);
public Computed<int> TotalValue => C(() => Gold.Value + Gems.Value * 10);
protected override void Build()
{
var label = new Label();
BindText(label, () => TotalValue.ToString());
}
}Computed<T> 会缓存自己的值,只在依赖变化时才重新计算。自动追踪:当 Value 执行 _fn() 时,其中读取的每一个 Prop<T>.Value 以及 Computed<T>.Value 都会自动成为一个依赖。
自 2026 年 7 月 1 日起: Computed<T> 实现了 IReactiveSource — 它本身就是一个响应式源:
Prop → Computed A → Computed B → BindText这样的链路可以正常工作(失效通知沿链路向上传递)BindText(label, () => MyComputed.Value)会把该 computed 作为一个源来订阅
Watch<T> — 追踪变化
public Prop<string> Status = new("idle");
protected override void Created()
{
Watch(Status, (oldVal, newVal) =>
{
if (newVal == "error")
PlayErrorAnimation();
});
}返回一个 IDisposable,用于手动取消订阅:
var handle = Watch(someProp, callback);
handle.Dispose(); // LaterWatchEffect(Action) — 自动追踪的 effect
public Prop<float> Health = new(100f);
public Prop<float> MaxHealth = new(150f);
protected override void Created()
{
WatchEffect(() =>
{
var ratio = Health.Value / MaxHealth.Value;
bar.style.width = Length.Percent(ratio * 100f);
});
}自动追踪 fn 内部读取的每一个 Prop<T> 和 Computed<T>,其中任意一个变化时都会重新执行 fn。返回一个用于取消订阅的 WatchHandle。
内部实现使用了 ReactiveEffect —— 这是所有 Bind* 方法和 WatchEffect 共同依赖的唯一响应式原语。组件被 detach 时,它的所有订阅都会自动清理(DisposeAllBindings)。
ReactiveEffect — 底层响应式原语(内部)
每一个绑定(BindText、BindShow、BindVisibility、BindClass、BindList、BindListFor)都建立在 ReactiveEffect 之上:
// Operating principle (simplified):
private WatchHandle ReactiveEffect(Action fn)
{
var subs = new List<IDisposable>();
void Run()
{
foreach (var s in subs) s.Dispose();
subs.Clear();
// Auto-track: collect all Prop/Computed read in fn
using (DependencyTracker.Track(src =>
subs.Add(src.SubscribeInvalidate(() => ScheduleBindUpdate(Run)))))
{
fn();
}
}
Run();
return new WatchHandle(() => { foreach (var s in subs) s.Dispose(); });
}关键特性:
fn()在DependencyTracker.Track()下执行 — 自动收集依赖- 为每一个源通过
SubscribeInvalidate订阅 - 失效时,通过
ScheduleBindUpdate以批处理方式重启(每帧一次) - 通过
HashSet<Action>去重 — 折叠频繁 setter 带来的重复失效
辅助方法
// P<T> is shorthand for new Prop<T>
public Prop<string> Title = P("Default Title");
// C<T> is shorthand for new Computed<T>
public Computed<bool> IsValid => C(() => !string.IsNullOrEmpty(Title));
// WatchEffect - auto-tracking
protected WatchHandle WatchEffect(Action fn);detach 时的清理
组件 detach 时,所有订阅(Bind*、Watch、WatchEffect)都会通过 OnDetachFromPanelHandler 中的 DisposeAllBindings() 自动清理。除非需要手动控制,否则无需显式调用 watch handle 的 Dispose()。
API
Prop<T>
public class Prop<T> : INotifyBindablePropertyChanged
{
public T Value { get; set; } // notifies subscribers
public event Action<T, T> Changed; // (old, new)
public static implicit operator T(Prop<T> p);
public Prop(T initial = default);
}Computed<T>
public class Computed<T> : IReactiveSource // itself is a source (push invalidation)
{
public T Value { get; } // cached, auto-invalidated
public static implicit operator T(Computed<T> c);
public Computed(Func<T> fn);
public void Invalidate(); // force mark dirty
public void Refresh(); // recalculate immediately
}读取
Computed<T>.Value会调用DependencyTracker.RegisterSource(this)— 因此外部追踪会把该 computed 视为一个源。MarkDirty()只在false→true的边沿把失效通知转发给订阅者。
WatchHandle
public class WatchHandle : IDisposable
{
public void Dispose(); // unsubscribe from Prop<T>
}IReactiveSource
public interface IReactiveSource
{
IDisposable SubscribeInvalidate(Action onInvalidate);
}Prop<T> 实现了 IReactiveSource。Computed<T> 用它来实现自动追踪。
DependencyTracker
internal static class DependencyTracker
{
public static IDisposable Track(Action<IReactiveSource> collector);
public static void RegisterSource(IReactiveSource source);
}[ThreadStatic] — 线程安全。不需要显式调用 DependsOn()。
组件之间的 props
适用于在 .sharq 中使用自定义组件(<sus:SusButton>)的场景。
字面量 prop
<!-- variant="primary" - mutates the .Value of an existing Prop, does not replace it -->
<sus:SusButton variant="primary" :text="Title" />生成器会输出 SetChildProp(child, "variant", "primary"),它会:
- 查找字段
Variant(不区分大小写,BindingFlags.IgnoreCase) - 如果该成员是一个非空的
Prop<T>→ 写入.Value(保留子组件内部的绑定) - 如果该成员是一个值为
null的Prop<T>→ 创建一个新实例 - 如果该成员是普通类型 → 直接赋值
响应式 prop
<!-- :variant="item.Kind" - reactive whenever item.Kind changes -->
<sus:SusButton :variant="item.Kind" />生成器会输出 BindChildProp(child, "variant", () => item.Kind),它会:
- 查找字段
Variant(不区分大小写) - 将其包裹进一个
ReactiveEffect— 自动订阅,并在 detach 时清理 - 修改现有
Prop<T>的.Value
标量转换
// ConvertScalar(value, targetType) supports:
string → bool (via bool.TryParse)
string → int (via Convert.ChangeType)
string → float (via Convert.ChangeType)
string → enum (via Enum.Parse, ignoreCase)
string → string (direct assignment)诊断
在 dev 构建中(#if DEVELOPMENT_BUILD || UNITY_EDITOR):
- 转换出错或遇到未知 prop 时会
LogWarning - 如果
BindChildProp按名称找不到匹配的Prop<T>成员,会LogError