跳转到内容

3. 响应式

更新时间:2026-07-01 — 新增通过 SetChildProp/BindChildProp 在组件之间传递 prop。

Prop<​T> — 响应式属性

csharp
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> — 计算属性

csharp
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> — 追踪变化

csharp
public Prop<string> Status = new("idle");

protected override void Created()
{
    Watch(Status, (oldVal, newVal) =>
    {
        if (newVal == "error")
            PlayErrorAnimation();
    });
}

返回一个 IDisposable,用于手动取消订阅:

csharp
var handle = Watch(someProp, callback);
handle.Dispose();  // Later

WatchEffect(Action) — 自动追踪的 effect

csharp
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 — 底层响应式原语(内部)

每一个绑定(BindTextBindShowBindVisibilityBindClassBindListBindListFor)都建立在 ReactiveEffect 之上:

csharp
// 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 带来的重复失效

辅助方法

csharp
// 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*WatchWatchEffect)都会通过 OnDetachFromPanelHandler 中的 DisposeAllBindings() 自动清理。除非需要手动控制,否则无需显式调用 watch handle 的 Dispose()

API

Prop<​T>

csharp
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>

csharp
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

csharp
public class WatchHandle : IDisposable
{
    public void Dispose();             // unsubscribe from Prop<T>
}

IReactiveSource

csharp
public interface IReactiveSource
{
    IDisposable SubscribeInvalidate(Action onInvalidate);
}

Prop<T> 实现了 IReactiveSourceComputed<T> 用它来实现自动追踪。

DependencyTracker

csharp
internal static class DependencyTracker
{
    public static IDisposable Track(Action<IReactiveSource> collector);
    public static void RegisterSource(IReactiveSource source);
}

[ThreadStatic] — 线程安全。不需要显式调用 DependsOn()


组件之间的 props

适用于在 .sharq 中使用自定义组件(<sus:SusButton>)的场景。

字面量 prop

xml
<!-- variant="primary" - mutates the .Value of an existing Prop, does not replace it -->
<sus:SusButton variant="primary" :text="Title" />

生成器会输出 SetChildProp(child, "variant", "primary"),它会:

  1. 查找字段 Variant(不区分大小写,BindingFlags.IgnoreCase
  2. 如果该成员是一个非空的 Prop<T> → 写入 .Value(保留子组件内部的绑定)
  3. 如果该成员是一个值为 nullProp<T> → 创建一个新实例
  4. 如果该成员是普通类型 → 直接赋值

响应式 prop

xml
<!-- :variant="item.Kind" - reactive whenever item.Kind changes -->
<sus:SusButton :variant="item.Kind" />

生成器会输出 BindChildProp(child, "variant", () => item.Kind),它会:

  1. 查找字段 Variant(不区分大小写)
  2. 将其包裹进一个 ReactiveEffect — 自动订阅,并在 detach 时清理
  3. 修改现有 Prop<T>.Value

标量转换

csharp
// ConvertScalar(value, targetType) supports:
stringbool (via bool.TryParse)
stringint (via Convert.ChangeType)
stringfloat (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