在开发新的渲染器核心时遇到了这个问题,着实有趣,遂提笔记录。

关于这个问题的大部分讨论都在go-app issues #1043,也可以去这里看截图。


我遇到的问题的症结在:go-app框架在刷新组件时,会对 (app.Compo).Render 的返回值进行判断,用于避免重复渲染相同内容。但是它判断的依据相当粗暴,仅仅是比较新的返回值与旧返回值的类型,类型相同则忽略其内在差异,直接视为相同内容一棍子打死。

针对这个问题,@oderwat封装 app.UI 并实现了一个 replacer ,代码在metatexx/go-app-pkgs。实现非常简明,我就直接pin在文章里了。

package mountpoint

import (
    "github.com/maxence-charriere/go-app/v9/pkg/app"
)

type replacer interface {
    app.Composer
    nr() int
    set(app.UI)
}

type mp0 struct {
    app.Compo
    n  int
    ui app.UI
}

func (c *mp0) nr() int {
    return c.n
}

func (c *mp0) set(el app.UI) {
    c.ui = el
}

func (c *mp0) Render() app.UI {
    return c.ui
}

type mp1 struct {
    app.Compo
    n  int
    ui app.UI
}

func (c *mp1) nr() int {
    return c.n
}

func (c *mp1) set(el app.UI) {
    c.ui = el
}

func (c *mp1) Render() app.UI {
    return c.ui
}

type UI struct {
    ui replacer
}

// Switch lets you switch the app.UI component with another one.
// It guarantees that the former component gets dismounted and
// the new one gets mounted in place of the old one.
func (c *UI) Switch(el app.UI) {
    if c.ui == el {
        return
    }
    if el.Mounted() {
        return
    }
    switch c.ui.nr() {
    case 0:
        c.ui = &mp1{ui: el, n: 1}
    case 1:
        c.ui = &mp0{ui: el}
    }
}

// UI returns the reference to the current mounted app.UI
func (m *UI) UI() app.UI {
    return m.ui
}

// New creates a new mountpoint for switching app.UI components
func New(ui app.UI) *UI {
    return &UI{&mp0{ui: ui}}
}

他在这里定义了 mp1mp2 两种类型,均为 replacer 的实现。其中包含 nr()set() 方法,其中 nr()field:n 用于获取和记录当前的状态,而 set() 用于更改replacer内部的component。然后定义了 UI 结构体,实现了 Switch()UI() 方法,用于传递当前生效的replacer。

它的工作原理是,即使原组件和新组件的类型一样,通过 (*UI).Switch()(*UI).UI() 方法,总能返回一个不一样的封装,即 mp1mp2 将交替作为返回值。而真正的组件被封装在它们的 field:ui,有了这层关系,即使 (*mp1/*mp2).Render 返回的是一样的类型,也将触发强制重新渲染。


Q.E.D.

Mio

2025-03-02 06:55 提笔
2025-03-02 07:10 完稿

最后修改:2025 年 03 月 02 日
如果这对你有用,我乐意之至。