> Linux新闻 >

Svelte 5 不是 JavaScript

在过去几周里,我一直忙于处理将一个 Web 应用程序升级到 Svelte 5 所带来的后果。抛开对框架更新换代和迁移烦恼的抱怨,我在迁移过程中遇到了一些有趣的问题。到目前为止,我没有看到很多人报告过相同的问题,所以我觉得自己阐述这些问题可能会有所帮助。

我会尽量不在这篇帖子中抱怨太多,因为我很感激多年来享受的 Svelte 3/4。但我想我不会再选择 Svelte 来开发任何新的项目了。我希望我在这里的一些反思对其他人也会有所帮助。

如果您对我在这里提到的问题的复现感兴趣,可以在以下链接找到。

无法将状态保存到 indexeddb

组件卸载导致闭包中的变量未定义

对速度的需求
首先,让我简要地认可一下 Svelte 团队所努力的方向。看起来版本 5 的大部分重大变化都是围绕 “深层响应性”(deep reactivity) 构建的,这允许更细粒度的响应性,从而带来更好的性能。这当然很好,Svelte 团队在性能与开发体验(DX)的协调方面一直表现出色。

在 Svelte 的早期版本中,实现这一目标的主要方式是通过 Svelte 编译器。涉及许多辅助技术来提高性能,但拥有一个框架编译步骤给了 Svelte 团队很大的灵活性,可以在幕后重新排列事物,而无需让开发者学习新概念。这就是 Svelte 最初如此独特的原因。

同时,这也导致了一个比以往更加晦涩难懂的系统框架,使得开发者调试更复杂的问题变得更加困难。更糟糕的是,编译器存在缺陷,导致了一些只能通过 “盲猜” 重构问题组件才能修复的错误。我个人至少遇到过五六次这样的情况,这也是我最终转向 Svelte 5 的原因。

尽管如此,我始终认为这是为了速度和生产力而可以接受的权衡。当然,有时我不得不删除我的项目,并将其迁移到一个新的仓库,但这个框架确实是一个使用起来的乐趣。

Svelte 5 更是加大了这种权衡的力度 —— 这是有意义的,因为这正是该框架与众不同的地方。这次的不同之处在于,抽象 / 性能的权衡并没有停留在编译器领域,而是以两种重要的方式侵入了运行时:

使用 proxies 来支持 deep reactivit

隐式组件生命周期状态。

这两个改动不仅提升了性能,还让开发者的 API 看起来更加整洁。为什么不喜欢呢?

不幸的是,这两个特性都是抽象泄漏的典型例子,最终是开发变得更加复杂,而不是更简单。

Proxies 不是 Objects
使用 proxies 似乎让 Svelte 团队能够在不要求开发者做额外工作的前提下,从框架中榨取更多性能。

在 React 等框架中,通过多个组件层级传递状态而不引发不必要的重新渲染,是一项臭名昭著的困难任务。 Svelte 的编译器避免了与虚拟 DOM 比较解决方案相关的一些陷阱,但显然仍有足够的性能提升,足以证明引入 proxies 的合理性。

Svelte 团队似乎也 认为 他们的引入代表了开发者体验的改进:

我们…… 可以最大化兼顾效率和人体工学。

问题是:Svelte 5 看起来 更简单,但实际上引入了 更多 的抽象。

使用 proxies 来监控数组方法很有吸引力,因为它允许开发者忘记确保状态是响应性的所有古怪启发式方法,只需向数组中 push 即可。我无法计算我在 Svelte 4 中写了多少次 value = value 来触发响应性。 在 Svelte 4 中,开发者必须了解 Svelte 编译器的工作原理。编译器作为一个有缺陷的抽象,迫使用户知道赋值是用来表示响应性的方式。在 Svelte 5 中,开发者可以 “忘记” 编译器!

但实际上,他们不能。所有新抽象的引入实际上只是引入了更多复杂的启发式方法,开发者必须将它们记在心里,以便让编译器按照他们的意愿工作。

事实上,这就是为什么在使用 Svelte 多年后,我发现自己在越来越多地使用 Svelte stores,而响应性声明则越来越少。原因在于 Svelte stores 就是 JavaScript。在 store 上调用 update 很简单,而且能够用 $ 来引用它们只是个额外的便利 —— 无需记住,如果编译器出错,它就会提醒我。

proxies 引入了与响应性声明类似的问题,那就是它们看起来像一件事,但在边缘上却表现得像另一件事。 当我开始使用 Svelte 5 时,一切运行得都很顺利 —— 直到我尝试将 proxies 保存到 indexeddb(GitHub 上的 issue),那时我遇到了 DataCloneError。更糟糕的是,没有通过 try/catch 结构化克隆来可靠地判断某个对象是否是 Proxy,这是一个性能密集型操作。

这迫使开发者记住哪些是 proxies,哪些不是,每次将 proxies 传递给一个不期望或不知道它们的上下文时,都要调用 $state.snapshot。这抵消了他们最初给予我们的所有美好抽象。

组件不是函数
虚拟 DOM 在 2013 年之所以能够流行起来,是因为它能够将应用程序建模为一系列组合函数,每个函数接收数据并输出 HTML。Svelte 保留了这种范式,使用编译器来规避虚拟 DOM 的低效和生命周期方法的复杂性。

在 Svelte 5 中,组件生命周期又回来了,采用了 react-hooks 风格。 在 React 中,hooks 是一种抽象,它允许开发者避免编写与组件生命周期方法相关的所有状态代码。现代 React 教程普遍推荐使用 hooks,这些 hooks 依赖于框架在不可见的方式下同步状态与渲染树。

虽然这确实会导致代码更简洁,但也要求开发者谨慎行事,以避免破坏围绕 hooks 的假设。只需尝试在 setTimeout 中访问状态,你就会明白我的意思。

Svelte 4 有几个类似的陷阱 —— 例如,与组件的 DOM 元素交互的异步代码必须跟踪组件是否已卸载。这和你在依赖生命周期方法的旧 React 组件中看到的那种模式非常相似。

在我看来,Svelte 5 通过添加与组件生命周期相关的隐式状态来协调状态变化和效果,似乎是走上了 React 16 的道路。 例如,以下是 $effect 文档的摘录:

您可以将 $effect 放置在任何位置,而不仅仅是组件的最顶层,只要它在组件初始化期间(或父级效果激活时)被调用。然后它将与组件(或父级效果)的生命周期相关联,因此当组件卸载(或父级效果被销毁)时,它将自动销毁。

这非常复杂!为了有效地使用 $effect...(抱歉),开发者必须理解状态变化是如何被追踪的。组件生命周期 文档 声称:

在 Svelte 5 中,组件的生命周期只包含两个部分:其创建和其销毁。介于两者之间的一切 —— 当某些状态更新时 —— 与组件整体无关;只有需要响应状态变化的那些部分才会收到通知。这是因为底层最小的变化单位实际上不是组件,而是组件在初始化时设置的(渲染)效果。因此,并没有 “更新前”/“更新后” 钩子这样的东西。

然而,它接着介绍了与 $effect.pre 结合的 “tick” 概念。本节解释说,“tick 返回一个 promise,它在任何挂起的州变化被应用后解决,或者在下一个微任务中如果没有挂起的变化时解决。”

我确信有一些心理模型可以证明这一点,但我不认为当必须紧接着关于状态变化的补充说明时,声称组件的生命周期仅由挂载 / 卸载组成真的很有帮助。 这个地方真正让我感到困扰,也是这篇博客帖子的动机所在,那就是当状态与组件的生命周期耦合在一起时,即使这个状态被传递给一个对 Svelte 一无所知的函数。

在我的应用程序中,我通过在存储中保存我想要渲染的组件及其属性来管理模态对话框,并在应用程序的 layout.svelte 中渲染它。这个存储也与浏览器历史同步,以便使用后退按钮关闭它们。有时,向这些模态之一传递一个回调是有用的,将调用者特定的功能绑定到子组件上:

const {value} = $props()
const callback = () => console.log(value)
const openModal = () => pushModal(MyModal, {callback})
这是 JavaScript 中的一个基本模式。传递回调只是你做的事情之一。

不幸的是,如果上述代码位于模态对话框本身中,调用组件会在回调被调用之前被卸载。在 Svelte 4 中,这运行得很好,但在 Svelte 5 中,当组件卸载时,value 会被更新为 undefined。这里有一个最小化复制的例子。

这只是一个例子,但对我来说,很明显,任何被生命周期比其组件长的回调函数封闭的属性,在我想要使用它时都会是 undefined—— 没有任何重新赋值存在于词法作用域中。

这根本不是 JavaScript 的工作方式。我认为 Svelte 之所以这样做,是因为它试图重新发明垃圾回收。因为 value 是组件的属性,它显然需要在组件生命周期的末尾被清理。我确信这背后有很好的工程原因,但这确实令人惊讶。

结论
简单的事情很美好,但正如 Rich Hickey 所说,简单的事情并不总是简单的。而且像 Joel Spolsky 一样,我不喜欢感到意外。Svelte 一直充满了魔法,但在我看来,随着最新版本的发布,重复咒语的认知成本终于超过了它赋予的力量。

在这篇文章中,我的目的并不是贬低 Svelte 团队。我知道很多人喜欢 Svelte 5(以及 react hooks)。我试图表达的观点是,在为用户做事和赋予用户自主权之间有一个权衡。好的软件是建立在理解之上,而不是聪明之上。

我也认为,随着 AI 辅助编码越来越受欢迎,记住这一点非常重要。不要选择让你与工作疏远的工具。选择那些利用你已经积累的智慧,并帮助你深化对这门学科理解的工具。 感谢 Rich Harris 及其团队多年来愉快的开发经历。我希望(如果你看到这段话的话),其中的不准确之处不至于影响作为用户反馈的价值。


(责任编辑:IT)