
本文最初發佈於 hodlbod 的個人部落格。
在過去的幾周裡,我一直在處理將一個 Web 應用程序升級到 Svelte 5 後所帶來的問題。除了框架不穩定以及遷移麻煩之外,在遷移過程中,我還遇到了其他一些有趣的問題。到目前為止,我還沒有看到其他人提出過同樣的問題,所以我覺得,我自己闡述下這些問題可能會比較有建設性。
在這篇文章中,我儘量不抱怨太多,因為我很感激,多年來我一直在使用 Svelte 3/4 愉快地進行開發。但這並不是說,今後的任何新專案我都會選擇 Svelte。我希望我在這裡的思考能對其他人有所幫助。
如果你想重現我在這裡提到的問題,可以檢視下面的連結:
-
無法將狀態儲存到 indexeddb
-
元件解除安裝導致閉包中的未定義變數
首先,請允許我簡單介紹一下 Svelte 團隊的工作目標。版本 5 中的大部分實質性變化似乎都是圍繞 “深度反應性”展開的。該特性可以提供更細粒度的反應性,從而帶來更好的效能。效能是個好東西,Svelte 團隊在平衡效能與 DX 方面一直表現出色。
在以前的 Svelte 版本中,實現這一目標的主要方法是使用 Svelte 編譯器。提高效能的過程會涉及到許多輔助技術,但框架編譯步驟為 Svelte 團隊提供了很大的餘地,讓他們可以重新安排底層的東西,而無需讓開發人員學習新概念。這正是 Svelte 最初的獨創之處。
同時,這也導致框架比以往更加不透明,遇到比較複雜的問題,開發人員更難除錯。更糟糕的是,編譯器有 Bug,由此導致的錯誤只能透過試著重構問題元件來修復。這種情況,我個人至少遇到過六次,這也是我最終遷移到 Svelte 5 的原因。
儘管如此,我始終認為,這種對速度和生產力的權衡是可以接受的。當然,有時我不得不刪除專案並將其移植到一個新的儲存庫,但這個框架確實讓我用得很開心。
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 鉤子)。我想說的是,在幫使用者做事和授予使用者代理權之間需要做好權衡。好的軟體基於對使用者的理解,而不是自作聰明。
我還認為,隨著人工智慧輔助編碼技術的日益普及,這是一條重要的教訓。不要選擇那些讓你與工作疏遠的工具。選擇那些可以利用你已經積累的智慧的工具,它們能幫助你加深對學科的理解。
原文連結:
https://hodlbod.npub.pro/post/1739830562159
宣告:本文由 InfoQ 翻譯,未經許可禁止轉載。
