跳转至

08 · 命令与订阅

异步工作走 CmdSubscription,不要在 View 或 Update 里散开无管理的 goroutine。

何时写完整 Program

ui.Run 始终接收 Program。需要下列任一能力时,使用 Program 字面量而不是 ui.NewProgram

  • 初始加载(Init 返回 Cmd)
  • 点击后请求网络 / 读写文件 / 延时
  • 定时器、监听流(Subscription)
  • 窗口状态消息

Cmd 生命周期

Update 改完 Model
    → 返回 Cmd
    → 运行时在独立 goroutine 执行 Cmd
    → Cmd 内 send(Msg) 入队
    → 后续帧再 Update

窗口销毁时,根 context.Context 会被取消。

编写 Cmd

DoContext(推荐阻塞/可失败工作)

func loadUser(id string) ui.Cmd[Msg] {
    // 在 Update 里准备好不可变快照
    return ui.DoContext(func(ctx context.Context, send ui.Send[Msg]) error {
        user, err := fetchUser(ctx, id) // 必须尊重 ctx
        if err != nil {
            return err // → OnError;或 send 一个失败消息后 return nil
        }
        send(UserLoaded{User: user})
        return nil
    })
}

Do(短、无 context)

ui.Do(func(send ui.Send[Msg]) {
    send(Tick{})
})

捕获规则(强制)

可以 不可以
拷贝后的 string、数字、struct 值 保留 *Model
显式拷贝的 slice/map 保留 *ui.Context
在 Update 中算好的快照 在 Cmd 里读会变的共享可变状态
// ✓
name := m.Name
return ui.DoContext(func(ctx context.Context, send ui.Send[Msg]) error {
    _ = name
    return nil
})

// ✗
return ui.DoContext(func(ctx context.Context, send ui.Send[Msg]) error {
    _ = m.Name // 不要捕获 model 指针/可变引用
    return nil
})

Batch

一次 Update 启动多个独立任务:

return ui.Batch(
    loadProfile(id),
    loadSettings(id),
)

不要在一个 Cmd 里自己 go 一堆任务又不统一错误/取消。

LatestCmd:只要最新结果

搜索、预览、自动完成:

return ui.LatestCmd("search", ui.DoContext(func(ctx context.Context, send ui.Send[Msg]) error {
    hits, err := search(ctx, query)
    if err != nil {
        return err
    }
    send(SearchResults{Hits: hits})
    return nil
}))
  • 同 key 的旧命令会被取消
  • 旧代消息会被丢弃
  • key 要稳定、数量有界(如 "search"),不要用每次请求的 UUID 当 key(key 会保留到窗口结束)

取消:

return ui.CancelLatestCmd[Msg]("search")

完整示例(延时加载)

examples/async

func Update(m *Model, msg Msg) ui.Cmd[Msg] {
    switch msg := msg.(type) {
    case Load:
        m.Loading = true
        return load()
    case Loaded:
        m.Loading = false
        m.Result = msg.Text
    }
    return nil
}

func load() ui.Cmd[Msg] {
    return ui.DoContext(func(ctx context.Context, send ui.Send[Msg]) error {
        t := time.NewTimer(time.Second)
        defer t.Stop()
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-t.C:
            send(Loaded{Text: "Loaded after one second."})
            return nil
        }
    })
}

Subscription

长生命周期输入:定时器、文件监听、websocket 等。

func subscriptions(m Model) []ui.Subscription[Msg] {
    if !m.Ticking {
        return nil
    }
    return []ui.Subscription[Msg]{
        ui.Subscribe("clock", func(ctx context.Context, send ui.Send[Msg]) error {
            ticker := time.NewTicker(time.Second)
            defer ticker.Stop()
            for {
                select {
                case <-ctx.Done():
                    return ctx.Err()
                case t := <-ticker.C:
                    send(Tick{At: t})
                }
            }
        }),
    }
}

规则:

说明
Key 生命周期身份;稳定则续跑,去掉则取消
空/重复 key 编程错误
结束后 不会每帧自动重启;要重试请改 key 或先移除再加入
消息 带 generation,停掉的代不会进 Update

嵌套模块:ui.MapSubscription(与 MapCmd 对称)。

错误处理

ui.Run(program,
    ui.OnError(func(err error) {
        log.Printf("flowui: %v", err)
    }),
)
  • Cmd/订阅错误、panicEffectError 等,到 OnError
  • 领域失败(登录失败、校验失败)→ 优先 send 成消息,写进 Model
  • Update/View panicRuntimePanicError,窗口循环停止

消息队列上限 256;溢出为 QueueOverflowError

MapCmd

cmd := child.Update(&m.Child, childMsg)
return ui.MapCmd(cmd, func(c ChildMsg) Msg {
    return ParentWrap{c}
})

不新开生命周期,只映射消息类型。

下一步