函数式与对象式:前端架构的两种成本曲线

摘要:人们习惯用"函数式 vs 对象式"划分 React/Vue 与 Zova 的阵营,但 React 的 Fiber 节点和 Vue 的组件实例揭示了一个事实:所谓函数式,底下都藏着一套对象系统。真正的分水岭不是语法,而是状态的身份、作用域与生命周期归谁所有------归闭包,还是归容器管理的 bean。本文用两条成本曲线重新解读这场范式之争:函数式后付"架构熵",对象式先付"仪式感";并给出三件只有容器化才能干净解决的硬需求(SSR 请求作用域、可拦截调用点、跨模块契约),以及对象式必须诚实面对的三个代价。最终的判断标准只有一句话:需要被找到、替换、共享、销毁的,就是对象;否则,就让它做函数。

一、先拆掉一个错误的对立

讨论 Vue/React 与 Zova 的差别时,最常见的说法是"函数式 vs 对象式"。这个说法很方便,但它把三对不同的东西混在了一起:

  • 语法形式:写函数还是写类
  • 状态宿主:闭包变量还是对象属性
  • 架构归属:身份、作用域、生命周期由谁管理

只要稍微往源码里走一步,就会发现"React 是函数式的、Vue 也是函数式的、Zova 是对象式的"这个分类根本站不住。

React 的"函数"只是渲染的表达式形式。 Hooks 依赖固定的调用顺序------这意味着每个函数组件背后都必须有一个持久存在的 Fiber 节点来保存 hooks 链表。纯函数不需要身份,而 React 组件有身份、有档期、有副作用清理队列。React 是把一套对象系统藏在了函数语法之下。

Vue 的 setup() 是函数,但每个组件实例是对象。 响应式宿主是 Proxy 对象,ref 是带 getter/setter 的盒子,reactive 是代理化的对象图。Vue 从来没有假装自己是纯函数式的。

所以真正的分水岭不在于"用不用函数",而在于:

谁拥有身份、作用域和生命周期------是闭包,还是容器管理的 bean?

这才是 Zova 与 Vue/React 默认心智模型的分界线。

二、两种模型各自的"默认答案"

同一个计数器,两种写法看起来只是语法差异:

typescript 复制代码
// Vue 3 默认心智模型:显式的响应式原语
const count = ref(0);
const count2 = computed(() => `=== ${count.value} ===`);
function increment() {
  count.value++;
}
tsx 复制代码
// Zova:框架托管的响应式对象
export class Counter {
  count: number = 0;
  count2: string;

  protected async __init__() {
    this.count2 = this.$computed(() => `=== ${this.count} ===`);
  }

  increment() {
    this.count++;
  }

  protected render() {
    return (
      <div>
        <div>{this.count2}</div>
        <button onClick={() => this.increment()}>Increment</button>
      </div>
    );
  }
}

差异不在于少写了 .value。差异在于:第一段代码里,状态的宿主是 setup() 的闭包,生命周期挂在组件实例上,共享需要另找机制;第二段代码里,状态的宿主是 bean 实例,这个实例先天地属于某个容器、某个作用域。

这一个区别,会在系统长大之后反复显形。

三、成本曲线:先付与后付

两种模型真正的差异是成本出现的位置。

函数式风格:架构熵,后付成本

单个组件几乎没有额外成本------写个函数,声明几个 ref,结束。但 Vue 官方给了四种彼此正交的共享机制:

  • props / emits
  • composables
  • provide / inject
  • store 层(Pinia 等)

当系统长到几十个模块、多个团队时,这四种机制会同时存在于一个代码库里。哪个状态该用哪种机制?没有强制答案,只能靠团队约定。约定的密度会随着人数线性增长,而执行的偏差会随人数指数增长。这就是架构熵------它不在第一天出现,但从不缺席。

对象式风格:仪式感,先付成本

Zova 里每个 bean 都要声明自己的角色、作用域、生命周期。一个只服务于单个组件内部的状态,也要先回答"你是哪个 bean、住在哪个容器里"。这是先付的仪式感。

换来的是共享机制收敛成一套:

typescript 复制代码
class ControllerPage {
  @Use()
  $$localCounterState: CounterState;          // ctx 容器:组件实例级

  @Use({ injectionScope: 'host' })
  $$hostCounterState: CounterState;           // 层级查找:父子共享

  @Use({ injectionScope: 'app' })
  $$appCounterState: CounterState;            // app 容器:应用/请求级

  @Use({ injectionScope: 'sys' })
  $$sysCounterState: CounterState;            // sys 容器:系统级
}

组件内部、组件之间、应用全局、系统全局------四种共享作用域,同一套 IoC 语义。provide/inject 不再是独立的子系统,而是 host / skipSelf 注入作用域的自然推论。

所以"哪个更好"是伪命题。 正确的问题是:你的系统复杂度主要落在哪一层?落在"渲染组合"上,函数式的灵活性是纯收益;落在"领域所有权、模块化、长期演进"上,对象式的统一性才能回本。

四、三件只有容器化才做得干净的事

有些事情不是"风格选择",而是"有没有这个机制"的问题。

1. SSR 的请求作用域

SSR 里有一条硬约束:跨请求的状态必须隔离,单例模块状态会泄漏用户数据。

函数式模型里没有"请求"这个可寻址的容器。你只有两个选择:模块级单例(泄漏),或者每次请求重建整个模块图(昂贵且容易漏)。而 Zova 的 app 容器天然就是请求边界,sys 容器天然跨请求------这不是语法糖,是 SSR 的一等需求被直接建模进了架构。

2. 可被拦截的调用点

AOP、权限注入、日志、行为扩展------这些都要求"调用点"有稳定身份。纯函数调用没有身份,想拦截只能靠包装函数层层套,而包装层自身又是匿名的、不可寻址的。bean 的方法调用天然穿过容器,容器天然是拦截点。

3. 跨模块契约

typescript 复制代码
class ControllerLayout {
  @Use('a-routertabs.model.tabs')
  $$modelTabs: ModelTabs;
}

bean identifier 让消费方依赖契约名,而非提供方的文件路径。模块可以重组、搬迁、替换实现,消费方不动。

函数式跨模块共享最终也会汇聚到某个全局 store key 或 context key------本质上也是字符串契约,只是没有容器替你管理它的注册、解析和生命周期。机制人人都会自己造,区别在于它是架构的一部分还是脚手架的一部分。

五、诚实地列出对象式的三个代价

如果只看到收益,那是营销不是架构。对象式有三个真实代价:

1. 装饰器 + 元数据 = 运行时魔法。 @Use() 背后是构建期转换和运行期解析的合谋。静态可读性下降,调用栈更深,排错时要多穿透一层框架。

2. 粒度被迫变粗。 当"任何东西都该是 bean"成为惯性,就会出现一个输入框也要立角色、定作用域的过度建模。对象式需要纪律来对抗自己的仪式感。

3. 与 React 演进方向的结构性冲突。 RSC 和并发渲染把状态推向可序列化的纯数据,而 bean 是不可序列化的活对象图。Zova 只能生长在 Vue 上而不是 React 上------这不是审美选择,是运行时底座决定的:Vue 的组件实例本来就是带响应式作用域的对象,天然适合作为 bean 的宿主;React 的 Fiber 底座则在往相反方向走。

六、收敛点:对象负责身份,函数负责表达

有意思的是,两个阵营都在向对方的核心地带移动。

Zova 自己就是混血:底层跑的是 Vue reactivity,$computed、$watch 是彻头彻尾的函数式 API。对象只承担身份、作用域、生命周期,不承担计算。

React 生态也在往身份化走:signals 的复兴、QueryClient 这样的外部状态容器、Zustand 这类"把状态从闭包里拿出来"的 store------本质都是承认纯闭包承载不了所有状态,需要一个可寻址的宿主。

由此可以给出一个可操作的判断标准:

一个东西如果需要被"找到、替换、跨作用域共享、随生命周期销毁",它就是对象;否则它就该是函数。

用对象包裹纯函数,是过度设计;用闭包承载需要身份的东西,是架构债。

七、结语:不在同一个抽象层上的争论

最后回到标题的那个对比。

函数式解决的是"如何组合渲染",对象式解决的是"如何组织一个长期存活的系统"。前者是编程范式,后者是架构范式------它们不在同一个抽象层上。拿两者的优雅程度直接对比,容易得出"FP 更先进"的结论,但这个结论是错的:React 的 hooks 恰恰是带顺序约束、带副作用、被 Fiber 调度的 effectful 闭包,离纯函数的距离并不比 class 更近。

真正值得问的问题始终是:

  • 我的系统里,状态的身份属于谁?
  • 状态共享机制是一套还是四套?
  • SSR 的请求边界建没建模?
  • 新人接手一个陌生模块,要改掉某个状态的行为:他需要在 composable、store、provide/inject 里逐一翻找状态的宿主,还是只问一句"这是哪个 bean"?

Zova 选择先付仪式感、后收秩序。这个选择对不对,不取决于语法的优雅程度,而取决于你的系统打算活多久、长多大。

进一步阅读

相关推荐
vx_Biye_Design2 小时前
django房屋租赁推荐系统78455-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·django·课程设计·express
林恒smileZAZ2 小时前
浏览器为什么加载不了超大图片
前端·vue.js·html5
Java后端的Ai之路7 小时前
前端技术Vue-13-安装webpack与vuejs
前端·vue.js·webpack
倚窗听花_7 小时前
IIS部署前端Vue项目
前端·javascript·vue.js
u0111026758 小时前
Vue 3 统一右侧编辑面板:布局、滚动与底部操作区设计
javascript·vue.js·ecmascript
change_fate8 小时前
Vue 3 Composable 函数指南
前端·javascript·vue.js
tryCbest8 小时前
Vue3——第 9 篇:组件通信全攻略(下)——Provide / Inject 与事件总线
前端·javascript·vue.js
溪语流沙10 小时前
【Web全栈进阶】Nginx反向代理 + HTTPS:正式上线
前端·vue.js·python·nginx·https·fastapi
用户2986985301410 小时前
React 前端 Word 转图片:页面与元素级导出的工程实践
前端·javascript·react.js