该换新神了:Vue → Solid
React组件在props发生变化时总是重新渲染。Vue的细粒度响应式能记住计算值的引用,只在依赖项变化时重算。可是,Vue在渲染组件时,还是会在任何prop有变时重渲染整个组件(#1181),即使变化的prop并未在渲染中使用。而Solid能做到如此细粒度。该换新神了。
另一方面,我在设计数据形状的时候愈发感到Vue的组件系统限制了想象力。
案例:表结构中的渲染函数
TanStack Table中对数据表结构的描述(列定义,column def)包含单元格的渲染方式,定义为函数组件。想要某字段显示为粗体?在React中,只要为该列定制JSX:
{
id: 'myField',
accessorFn: row => row.myField,
header: '我的字段',
cell: ({ row }) => <b>{row.myField}</b>,
}
渲染函数跟其他与列相关的信息集中保存在一处,使列定义可复用。
Vue渲染层也采用虚拟DOM,安装了JSX插件的话,也可以用上述方法为列指定渲染方式。反之,则需要手写h
。
import { h } from 'vue'
{
cell: ({ row }) => h('b', [row.myField]),
Vue社区并不青睐JSX和手写渲染函数。Vue特有的模板语法(v-model
和自定义directive)不能地道地以JSX表达,Vue编译器的模板优化也无法应用于手写的渲染函数。不能使用Vue模板语法无疑增添了学习负担。
需要向子组件传递渲染函数的场合,Vue风格的写法是slot。slot实参需要当场写在模板中。上述数据表格的场景中,渲染函数跟其他列信息逻辑上相关联,单独将渲染函数拆到模板中既分散了信息又降低了可复用性,因此使用slot也不是个好的解决方案。
组件系统不够灵活,为API设计带来了阻碍。
灾难性的类型支持
JS灵活得过分,新的接口应面向TS设计。先有API再适配类型的话,很容易陷入困境。
Vue组件的类型负担
React的函数组件将组件的类型简化到了(props: {}) => JSX.Element
,定义和使用都很轻松,得以无负担地嵌入各种数据结构定义中。Vue组件继承了选项式API时代的普通对象,类型使用起来十分复杂。这是标注组件需要使用的类型:
/**
* A type used in public APIs where a component type is expected.
*/
export type Component<
Props = any,
RawBindings = any,
D = any, // data
C extends ComputedOptions = ComputedOptions,
M extends MethodOptions = MethodOptions,
E extends EmitsOptions | Record<string, any[]> = {},
S extends Record<string, any> = any, // slots
> =
| ConcreteComponent<Props, RawBindings, D, C, M, E, S>
| ComponentPublicInstanceConstructor<Props>
Vue组件的事件不像其他框架那样是以prop形式传递的函数,而是一套与prop并行的emit系统,因此组件接口不仅要指定的prop的类型,还要指定emit的类型。在Component类型中,参数E在相当靠后的位置,很难指定上。
SFC LSP
自定义的语法很难做到跟TS本体一样程度的支持,时隔多年才支持泛型组件便是证明。
Vue的类型报错犹如C++模板一般,一方面是因为类型本身的复杂性,另一方面Vue language server也干了。Vite插件和VLS都采取将SFC转译为TS代码的策略,在转译时插入模板等非TS的信息。TS编译器作为后续步骤,报的错就掺杂了这些并非用户自己编写的类型,常常令人一头雾水。
存在感太强的组件边界
这是React和Vue共有的问题:组件划分的粗细影响渲染性能。但React是因为设计如此,Vue则不然。
Vue的响应式系统是细粒度的,渲染却对齐组件边界。Vue明明有真响应式系统@vue/reactivity,却连watch(对应React的useEffect)都不在其中,因为watch依赖组件的生命周期。inject和provide(对应React的context)也依赖组件层级。Vue文档建议在渲染大列表时减少组件嵌套,原因是创建组件的开销不可忽略。
[…] do keep in mind that component instances are much more expensive than plain DOM nodes, and creating too many of them due to abstraction patterns will incur performance costs.
Note that reducing only a few instances won't have noticeable effect, so don't sweat it if the component is rendered only a few times in the app. The best scenario to consider this optimization is again in large lists. […]
叛变
我现在明白Solid的初衷了。React的TS支持和配套工具比Vue好很多,但组件与渲染绑定,很容易产生渲染性能问题。Solid解决的核心问题,是将组件的划分与渲染彻底解耦。提取组件就如同提取函数一样自然,想提就提,没有其他副作用需要考虑。(参照Solid作者Ryan Carniato的文章Components are Pure Overhead。)
除了React风格的组件即函数,Solid的响应式基元还统一了Vue中需要通过toRef归一的ref和getter:signal本身就是getter。Solid的API设计似乎有一种大力出奇迹的美感:用最少量的魔法获得了最好的可组合性。
我花了几天时间把我的项目从Vue迁移到了Solid。两者的语法差异很大,但思考方式几乎没有变化。我宣布,我已经不是Vue教徒了。