写点什么

Svelte 不是 JavaScript

  • 2025-03-13
    北京
  • 本文字数:3618 字

    阅读完需:约 12 分钟

大小:1.70M时长:09:53
Svelte 不是 JavaScript

本文最初发布于 hodlbod 的个人博客。



在过去的几周里,我一直在处理将一个 Web 应用程序升级到 Svelte 5 后所带来的问题。除了框架不稳定以及迁移麻烦之外,在迁移过程中,我还遇到了其他一些有趣的问题。到目前为止,我还没有看到其他人提出过同样的问题,所以我觉得,我自己阐述下这些问题可能会比较有建设性。

 

在这篇文章中,我尽量不抱怨太多,因为我很感激,多年来我一直在使用 Svelte 3/4 愉快地进行开发。但这并不是说,今后的任何新项目我都会选择 Svelte。我希望我在这里的思考能对其他人有所帮助。

 

如果你想重现我在这里提到的问题,可以查看下面的链接:

速度要求

首先,请允许我简单介绍一下 Svelte 团队的工作目标。版本 5 中的大部分实质性变化似乎都是围绕 “深度反应性”展开的。该特性可以提供更细粒度的反应性,从而带来更好的性能。性能是个好东西,Svelte 团队在平衡性能与 DX 方面一直表现出色。

 

在以前的 Svelte 版本中,实现这一目标的主要方法是使用 Svelte 编译器。提高性能的过程会涉及到许多辅助技术,但框架编译步骤为 Svelte 团队提供了很大的余地,让他们可以重新安排底层的东西,而无需让开发人员学习新概念。这正是 Svelte 最初的独创之处。

 

同时,这也导致框架比以往更加不透明,遇到比较复杂的问题,开发人员更难调试。更糟糕的是,编译器有 Bug,由此导致的错误只能通过试着重构问题组件来修复。这种情况,我个人至少遇到过六次,这也是我最终迁移到 Svelte 5 的原因。

 

尽管如此,我始终认为,这种对速度和生产力的权衡是可以接受的。当然,有时我不得不删除项目并将其移植到一个新的存储库,但这个框架确实让我用得很开心。

Svelte 不是 Javascript

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

  • 使用代理支持深度反应性

  • 隐式组件生命周期状态

 

这两项改动都提高了性能,使开发人员使用的 API 看起来更加流畅。这有什么不好吗?很遗憾,这两项功能都是抽象泄漏的典型案例。最终,它们只会增加开发人员的工作复杂度,而不是降低复杂度。

代理不是对象

代理的使用似乎让 Svelte 团队从框架中榨取了更多的性能,而且不用要求开发人员做任何额外的工作。在 React 等框架中,在不引起不必要的重新渲染的情况下,在多层组件中线程化状态是一件非常困难的事情。

 

Svelte 的编译器避免了一些与虚拟 DOM 差异对比解决方案相关的陷阱。而且显然,性能增益仍然足以证明引入代理的合理性。Svelte 团队似乎还认为,引入代理改善了开发体验:

我们可以最大限度地提高效率和人体工程学。

问题就在这里:Svelte 5 看起来更简单,但实际上引入了更多抽象。

 

使用代理来监控数组方法(例如)之所以吸引人,是因为它允许开发人员忘掉所有愚蠢的启发式方法,只需将其push到数组即可确保状态是反应性的。在 Svelte 4 中,我不知道写了多少次 value = value 来触发反应性。

 

在 Svelte 4 中,开发人员必须了解 Svelte 编译器是如何工作的。Svelte 的编译器存在抽象泄漏,它迫使用户必须要知道,如何通过赋值发送反应性信号。在 Svelte 5 中,开发人员可以”忘记“编译器。

 

但他们不能。实际上,新引入的抽象只是引入了更复杂的启发式方法,开发人员必须将其牢记于心,才能让编译器按照他们希望的方式运行。

 

事实上,这就是为什么在使用 Svelte 多年后,我发现自己越来越频繁地使用 Svelte 存储,而较少使用反应式声明的原因。这是因为,Svelte 存储只是 javascript,在上面调用update非常简单。而且,还有一个额外的好处是能用$引用它们——不用记住任何东西,如果我弄错了,编译器就会发出警告。

 

代理引入了一个与反应式声明类似的问题,即它们看起来像一个东西,但在一些特殊情况下却像另一个东西。

 

当我开始使用 Svelte 5 时,一切都很好——直到我试图将代理保存到 indexeddb,我遇到了一个错误 DataCloneError。更糟糕的是,如果不try/catch一个结构化克隆,就无法可靠地判断某个东西是否是Proxy,而那是一个性能密集型操作。

 

这就使得开发人员不得不记住什么是代理,什么不是代理,每次将代理传递给不知道代理的上下文时,都要调用 $state.snapshot。这就破坏了它最初为我们提供的所有良好的抽象。

组件不是函数

虚拟 DOM 早在 2013 年就已风靡全球,其原因在于它能将应用程序建模为由多个函数组成的模型,每个函数都能获取数据并输出 HTML。Svelte 保留了这种模式,使用编译器来避免虚拟 DOM 的低效和生命周期方法的复杂性。

 

在 Svelte 5 中,组件生命周期以 react-hooks 的方式回归。

 

在 React 中,钩子是一种抽象,使得开发人员不用再编写所有与组件生命周期方法相关的有状态代码。现代 React 教程普遍建议使用钩子,因为钩子依赖于框架与渲染树的隐式状态同步。

 

虽然这确实能让代码更简洁,但也要求开发人员小心谨慎,避免破坏与钩子相关的假设。只要尝试一下在 setTimeout 中访问状态,你就会明白我的意思。

 

Svelte 4 有一些类似的问题。例如,与组件的 DOM 元素交互的异步代码必须跟踪组件是否已卸载。这与依赖生命周期方法的旧版 React 组件所使用的模式非常类似。

 

在我看来,Svelte 5 走的似乎是 React 16 的路线,通过添加与组件生命周期相关的隐式状态来协调状态变化和效果。

 

例如,以下内容摘自介绍 $effect 的文档:

你可以将 $effect 放在任何地方,而不仅仅是组件的顶层,只要在组件初始化时(或父 effect 处于激活状态时)调用即可。这样,它就与组件(或父 effect)的生命周期绑定在了一起,因此,当组件卸载(或父 effect 销毁)时,它就会自行销毁。

这非常复杂!为了有效使用 $effect,开发人员必须了解如何跟踪状态变化。介绍组件生命周期的文档里这样写道:

在 Svelte 5 中,组件生命周期仅包含两部分: 创建和销毁。中间的一切——当某些状态更新时——都与整个组件无关;只有需要根据状态变化做出反应的部分才会收到通知。这是因为在后台,变化的最小单位实际上不是组件,而是组件初始化时设置的(渲染)效果。因此,不存在 “更新前”/“更新后”钩子。

文档接着介绍了如何搭配使用 tick$effect.pre 。这一部分解释说:”tick返回一个 promise ,一旦任何待处理的状态变化被应用,就对它进行解析,如果状态没有变化,则在下一个微任务中解析。“

 

我相信,有些心智模型可以证明这一点,但对于组件的生命周期只包括挂载/卸载这个说法,我不认为真的有什么帮助,因为在挂载/卸载之后,还必须有一个关于状态变化的 addendum‌。

 

当状态与组件的生命周期结合在一起时,甚至当状态被传递给另一个对 Svelte 一无所知的函数时,这才是真正让我头疼的地方,也是我写这篇博文的动机。

 

在我的应用程序中,我管理模态对话框的方法是将要渲染的组件及其 props 存储在一个 store 中,然后在应用程序的 layout.svelte文件中进行渲染。这个 store 还与浏览器历史保持同步,这样后退按钮就可以关闭它们。有时,向其中一个模态对话框传递回调,将特定于调用者的功能绑定到子组件上也很有用:

const {value} = $props()    const callback = () => console.log(value)    const openModal = () => pushModal(MyModal, {callback})
复制代码

这是 JavaScript 中的一种基本模式。传递回调只是其中之一。

 

遗憾的是,如果上述代码本身位于模态对话框中,那么在调用回调之前,调用者组件就会被卸载。在 Svelte 4 中,这样做没什么问题,但在 Svelte 5 中,当组件被卸载时,value会被更新为undefined这里有一个最简单的重现

 

这只是一个例子,但显然,任何被回调函数关闭的 prop ,如果其生命周期长于其组件的生命周期,那么当我想要使用它时,它将是未定义的——在词法作用域中不会重新赋值。

 

这不是 JavaScript 的工作方式。我认为, Svelte 采用这种方式工作的原因在于它试图彻底改造垃圾回收。因为value 是组件的 prop ,所以很显然,必须在组件生命周期结束时进行清理。我相信,这其中一定有理由充分的工程设计原因,但也确实让人吃惊。

小结

事情容易固然好,但正如 Rich Hickey 所说,容易的事情并不一定简单。就像 Joel Spolsky 所说的一样,我不喜欢惊喜。Svelte 一直充满魔力,但随着最新版本的发布,我认为认知开销已经超过了它所带来的力量。

 

我写这篇文章的目的不是要抨击 Svelte 团队。我知道很多人都喜欢 Svelte 5(以及 react 钩子)。我想说的是,在帮用户做事和授予用户代理权之间需要做好权衡。好的软件基于对用户的理解,而不是自作聪明。

 

我还认为,随着人工智能辅助编码技术的日益普及,这是一条重要的教训。不要选择那些让你与工作疏远的工具。选择那些可以利用你已经积累的智慧的工具,它们能帮助你加深对学科的理解。

 

声明:本文为 InfoQ 翻译,未经许可禁止转载。

 

原文链接:https://hodlbod.npub.pro/post/1739830562159

2025-03-13 18:531

评论

发布
暂无评论

CompletableFuture实现异步转同步

FunTester

基于GIS+WebGL智慧消防3D可视化云控系统

2D3D前端可视化开发

智慧消防 消防物联网云平台 消防三维可视化 智慧消防系统 消防云控平台

SparK 用稀疏掩码为卷积设计 Bert 预训练

Zilliz

计算机视觉

RocketMQ Streams拓扑构建与数据处理过程

Apache RocketMQ

RocketMQ 消息列队

理论+实践,教你如何使用Nginx实现限流

华为云开发者联盟

后端 开发 华为云 企业号 2 月 PK 榜 华为云开发者联盟

如何用Apipost校验响应结果

爱研究代码的极客人

APi设计 JSON Schema apipost

单线程 Redis 如此之快的 4 个原因

C++后台开发

redis 中间件 后端开发 单线程 C++开发

易观千帆 | 12月用户体验GX评测:国有行及股份行持续领跑,农信社用户体验关注提升

易观分析

金融 手机银行

9种跨域方式实现原理

华为云开发者联盟

开发 华为云 企业号 2 月 PK 榜 华为云开发者联盟

OpenHarmony标准系统内核学习【2】CPU轻量级隔离特性

离北况归

OpenHarmony

泰山众筹sun4.0矩阵合约系统开发搭建

开发微hkkf5566

浪潮云:以数据云IBP释放数据要素力量

云计算 数据云

如何让OpenHarmony编译速度“狂飙”

离北况归

OpenHarmony

实战分享 | 金融数据采集报送平台实践

葡萄城技术团队

聊聊Docker镜像

天翼云开发者社区

Docker 镜像

线上网络丢包引起的接口响应时间过慢,快速排查案例

KINDLING

Java 运维 网络 丢包 eBPF&Linux

全景剖析阿里云容器网络数据链路(三):Terway ENIIP

阿里巴巴云原生

阿里云 云原生 云原生容器

如何通过jstat命令进行查看堆内存使用情况

华为云开发者联盟

后端 开发 华为云 企业号 2 月 PK 榜 华为云开发者联盟

工业生产环境下,时序数据库 TDengine 如何打造全面有效的数字化监控?

TDengine

数据库 tdengine 时序数据库

LeaRun快速开发平台:自由搭建个性化门户

力软低代码开发平台

APISIX Ingress 如何使用 Cert Manager 管理证书

API7.ai 技术团队

证书 api 网关 APISIX Ingress Controller

行云洞见|为何行业权威都预测“云原生IDE 将成为常态”?

行云创新

ide 云原生 云端IDE Cloud IDE TitanIDE

谈谈我工作中的23个设计模式

阿里巴巴中间件

阿里云 云原生

NFTScan 正式上线 Fantom 网络 NFTScan 浏览器和 NFT API 数据服务

NFT Research

NFT 数据基础设施

MySQL中的distinct和group by哪个效率更高?

Steven

简单概述Serverless

天翼云开发者社区

单线程架构的Redis如此之快的 4 个原因

JAVA旭阳

redis 缓存

新年新气象,老兵开新坑

致知Fighting

Java Go 服务器

一文教你如何重新认识用户

蔡农曰

互联网 产品经理 消费者 需求设计

Svelte 不是 JavaScript_架构/框架_hodlbod_InfoQ精选文章