专栏名称: OSC开源社区
OSChina 开源中国 官方微信账号
目录
相关文章推荐
程序猿  ·  “我真的受够了Ubuntu!” ·  昨天  
程序员的那些事  ·  李彦宏自曝开源真相:从骂“智商税”到送出“史 ... ·  3 天前  
OSC开源社区  ·  李彦宏:DeepSeek让我们明白要将最优秀 ... ·  3 天前  
OSC开源社区  ·  大模型训练中的开源数据和算法:机遇及挑战 ·  3 天前  
51好读  ›  专栏  ›  OSC开源社区

升级到Svelte 5后,我不会再用Svelte开发新项目

OSC开源社区  · 公众号  · 程序员  · 2025-02-20 16:30

正文

OSCHINA

↑点击蓝字 关注我们


🔗 《2024 中国开源开发者报告》正式发布


在线阅读: https://talk.gitee.com/report/china-open-source-2024-annual-report.pdf




在过去几周里,我一直忙于处理将一个 Web 应用程序升级到 Svelte 5 所带来的后果。抛开对框架更新换代和迁移烦恼的抱怨,我在迁移过程中遇到了一些有趣的问题。

到目前为止,我没有看到很多人报告过相同的问题,所以我觉得自己阐述这些问题可能会有所帮助。

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

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

  • 无法将状态保存到 indexeddb
    https://github.com/sveltejs/svelte/issues/15327

  • 组件卸载导致闭包中的变量未定义
    https://github.com/sveltejs/svelte/issues/15327

对速度的需求

首先,让我简要地认可一下 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 及其团队多年来愉快的开发经历。我希望(如果你看到这段话的话),其中的不准确之处不至于影响作为用户反馈的价值。



相关阅读
2024前端现状:开发者最爱用React、最想学习Svelte
今年最火开源前端框架——Svelte 5正式发布稳定版、彻底重写、新增$语法 、star数近8万






请到「今天看啥」查看全文