[譯]為何Svelte殺不死React

NO IMAGE

為何 Svelte 殺不死 React

是僅現狀造成的嗎?還是說只因為 React 更強大?

當我剛剛開始讀 Svelte 的文檔時,我發現這東西太振奮人心了,我簡直想要在 Medium 上寫封表揚信給它。而當我讀完來自官方博客和社區的一些文章後,我就冷靜下來了,因為我注意到了一些在 JavaScript 世界中很常見的言辭 —— 這種言辭讓我非常苦惱。

嘿,是否還記得那個 30 年來人類絞盡腦汁想要解決的問題?我剛剛發現了一個通用的解決方案!為什麼它還沒征服全世界?這多麼顯而易見啊。Facebook 的營銷團隊正在密謀對付我們。

在我看來,你可以說你的工具與現有的工具相比是革命性的。而且人很難對自己的作品保持完全公正的態度,這我能理解。舉個正面的例子 —— 與其他解決方案比起來,我覺得 Vue 實在是幹得漂亮。沒錯,確實存在一些我不敢苟同的質疑聲音,但這些聲音都在傳達一個建設性的信息:

我們的解決方案是怎樣怎樣的,還有別的一些現有的解決方案。而且我們堅信我們的方案更優秀,原因是什麼什麼。一些常見的反對論點是什麼什麼。

Svelte 的官方博客卻正好相反,它通過只顯露片面的事實來愚弄讀者,甚至有時會宣揚一些關於 Web 技術和其他庫(我會著重提到 React,只因我對它的理解更深一些)的不實言論。因此在本文中,我會對 Svelte 調侃一二,平衡一下官方吹斜的天平。話雖如此,我仍認為 Svelte 中還是有閃光點的,我會在文末告訴你原因 😊

[譯]為何Svelte殺不死React

何為 Svelte?

Svelte 是一個構建用戶界面的工具。主流的框架 —— 如 React 和 Vue —— 都是利用虛擬 DOM 根據組件輸出進行高效的 DOM 更新,而 Svelte 沒有走這條路線,它使用靜態分析,在運行時創建 DOM 更新代碼1。 一個 Svelte 組件長這樣:

App.svelte

<script>
import Thing from './Thing.svelte';
let things = [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
];
function handleClick() {
things = things.slice(1);
}
</script>
<button on:click={handleClick}>
Remove first thing
</button>
{#each things as thing}
<Thing color={thing.color}/>
{/each}

Thing.svelte

<script>
export let color;
</script>
<p>
<span style="background-color: {color}">current</span>
</p>
<style>
span {
display: inline-block;
padding: 0.2em 0.5em;
margin: 0 0.2em 0.2em 0;
width: 4em;
text-align: center;
border-radius: 0.2em;
color: white;
}
</style>

而對應的 React 組件是這樣的:

import React, {useState} from 'react'
import styled from 'styled-components';
const things = [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
];
const Block = styled.span`
display: inline-block;
padding: 0.2em 0.5em;
margin: 0 0.2em 0.2em 0;
width: 4em;
text-align: center;
border-radius: 0.2em;
color: white;
background-color: ${props => props.backgroundColor}
`;
const Thing = ({color}) => {
return (
<p>
<Block backgroundColor={color} />
</p>
);
}
export const App = () => {
const [things, setThings] = useState(things);
const removeFirstThing = () => setThings(things.slice(1))
return (
<>
<button onClick={removeFirstThing} />
{things.map(thing =>
<Thing key={thing.key} color={thing.color} />
}
</>
);
}

Svelte 不是一個框架 —— 它是一種語言

Svelte 簡單地用 <script><style> 創建 Vue 風格的“單文件組件”。該語言中增加了一些結構,用以解決 UI 開發中最大的問題之一 —— 狀態管理。

我在上一篇文章提到了幾個在 React 裡用 JavaScript 解決此問題的方法。Svelte 藉助其作為編譯器之便利,使得響應性成為該語言的特性 2。Svelte 引入了兩種新型的語言結構來達到這個目的。

  • 在語句前加 $: 能夠讓該語句具有響應性的運算符,即每當該語句讀取的變量有更新時,它都會被重新執行。一個語句可以是一次賦值(即“依賴變量”或“派生變量”)、一個代碼塊或者一個調用(即“作用”)。這有點類似 MobX 的方式,只不過是集成到語言中了。
  • $ 一個能創建指向倉庫(存儲狀態的容器)的訂閱的運算符,當組件解除掛載時,該訂閱即被自動取消。

Svelte 的響應性概念使我們能夠使用常規的 JavaScript 變量存儲狀態 —— 不再需要狀態容器了。但這樣做真的提升了開發體驗(DX)嗎?

[譯]為何Svelte殺不死React

Svelte 的響應性

React 最初的承諾是,你可以在每次狀態改變時重新渲染整個應用,而無需擔心性能問題。而實際上,我認為那不準確。如果確實如此,那像 shouldComponentUpdate(一種告訴 React 何時可以安全跳過一個組件的方法)這種優化就沒有存在的必要了 —— Rich Harris,Svelte 的維護者3

真正的問題在於,程序員花費了大量的時間在錯誤的地點和時間去擔心效率;在編程中,過早優化是萬惡(或者至少大部分)之源。—— Donald Knuth,美國計算機科學家4

首先,我們得搞清楚一點。就算你的代碼中沒有任何 shouldComponentUpdate 這類優化標識,React 也不會在每個狀態改變時就重新渲染整個應用。這很容易驗證 —— 你只需在應用的根組件調用一次 console.log

[譯]為何Svelte殺不死React

在此例中,除非 isAuthorized 發生改變,否則 App 不會被重新渲染。任何子組件的改變都不會導致 App 重新渲染。僅當組件自己的狀態改變,或者被 React Context 觸發,或者父組件重新渲染時,它才會被重新渲染。

最後一種情況導致了所謂的無用渲染 —— 預先知道父組件重新渲染不會導致子組件 DOM 層級發生任何改變時的渲染。這種無用渲染髮生在子組件的 prop 不可變,或者這種改變不會影響可視界面的情況中。你可以通過定義 shouldComponentUpdate(或者使用 React.memo 作為一個更現代化的功能備選方案)來避免無用渲染。

僅在特殊情況下優化,不要默認開啟優化

在絕大多數情況下,無用渲染並沒有什麼壞處。它們耗費的資源小到了肉眼不可見的程度。事實上,對每個組件的 prop 進行淺層(我甚至都不用說深層)的前後比較,比簡單粗暴地重新渲染整個子樹佔用的資源還多。這就是為什麼 React 回退到把 shouldComponentUpdate: () => true 作為默認設置。此外,React 團隊甚至在調試工具中移除了“highlight updates”特性,因為此前人們習慣毫無根據的優化任何一個無用渲染5

這是一個非常危險的做法,因為每次優化都意味著要做假設。當你壓縮一張圖片時,你就會假設有些負載可以在不影響質量的前提下被削減,當你向後端增加緩存數據時,你就會假設 API 可能會返回相同的結果。恰當的假設能讓你節省資源。而不恰當的假設就會給應用帶來 Bug。這就是應該合理做優化的原因。

Svelte 選擇了相反的處理方式。除非你用 $: 運算符做出了明確指定,否則組件代碼不會在更新時重新運行。我可不想花上幾十個小時來查找我哪個地方忘了加 $: 運算符,並試圖搞清楚為何我的應用跑不起來 —— 只為了用戶能享受那快了 20 毫秒的重渲染。如果偶然遇到一個體積龐大的組件,我確實會優化它,但那是極其少見的情況了。在這一點上死扣開發體驗是沒有意義的。

Svelte 的優化不夠優

順便說,如果我們要在技術上較真,其實 Svelte 檢查某個更新是否必需的結果也不總是最優的。假設一個組件的計算開銷非常大,它接受一個這樣的 prop:Array<{id: string, otherProps}>。假設我已知 id 都是唯一的,數組中的元素是不可變的,我可以通過下列代碼得出某個更新是否必要:

const shouldUpdate = (prevArr, nextArr) => {
if (prevArr.length !== nextArr.length) return true;
return nextArr.some((item, index) => item.id !== prevArr[index].id)
}

在 Svelte 中,無法指定自定義的反應比較器(Reaction comparator),只能像這樣比較數組:

export function safe_not_equal(a, b) { 
return a != a ? b == b : a !== b 
|| ((a && typeof a === 'object') || typeof a === 'function');
}

我可以使用一些第三方內存工具給 Svelte 的比較器打個補丁,這我能接受,但我在意的是 —— 世上沒有仙丹神藥,優化得“過了頭”就會造成束手束腳的限制。

意義不明的狀態更新

在 React 中,當你想要更新狀態,你必須調用 setState。而在 Svelte 中,要更新狀態,你得這樣:

…… 被更新的變量的名字必須位於賦值運算的左側。

Svelte 很神奇地給內部運行時用於出發反應的空函數添加一個調用。這可能會讓人抓狂。

const foo = obj.foo;
foo.bar = 'baz';
obj = obj; // 如果你不這樣做,更新就不會發生

同樣,用 push 或或其他變種方法更新一個數組,都不會自動觸發組件更新。因此你必須用數組或對象擴展:

arr = [...arr, newItem];
obj = {...obj, updatedValue: newValue};

這跟在 React 中基本一致,除了在 React 裡你要調用函數並把被更新的狀態傳給函數,而在 Svelte 中你會有種正在處理常規的可變變量的錯覺。這種體驗會從某種程度上降低 Svelte 的優勢,降低你發出“哇哦你看太酷了,Svelte 是一個編譯器”這種驚歎的衝動。

虛擬 DOM

虛擬 DOM 很有價值,因為它使你能在構建應用時不用考慮狀態轉變,並且性能一般都足夠強勁 —— Rich Harris,Svelte 的維護者6

Svelte 博客中幾乎每篇文章都聲稱,虛擬 DOM是一個不必要的開銷,而且開銷相當大,可以輕易地用預先生成的 DOM 更新器替換它並且無副作用。但這句話對嗎?不全對。

[譯]為何Svelte殺不死React

虛擬 DOM 會增加開銷嗎?

是的,肯定會。虛擬 DOM 不是個特性,把它放進應用中,並不能妙手回春地讓潛在的“真實”DOM 和瀏覽器跑得更快。它只是一種將易寫、易讀、易調試的聲明式代碼轉為高效、易於執行的命令式 DOM 操作。

但開銷就一定是不好的嗎?我覺得不是 —— 否則 Svelte 的維護者就得用 Rust 或 C 來寫他們的編譯器了,因為 JavaScript 的垃圾收集器就是最大的開銷。我猜在決定編譯器的技術棧時,他們做了一個權衡 —— 開銷有多高與社區得到的好處有多大之間的取捨。在這種情況下,開銷相對不高 —— 設備上並沒有一直在運行的編譯器,你只是時不時地運行它而已,涉及到的計算不多,幾秒鐘的時間不會給用戶體驗造成很大影響。另一方面,因為 Svelte 基於 JavaScript,並把 JavaScript 作為執行環境,用 TypeScript/JavaScript 開發的工具為開發體驗提供了相對可觀的好處:每個對此工具感興趣的人 —— 因此想貢獻代碼或需要學習編譯器源代碼 —— 可能都是瞭解 JavaScript 的人。

因此,對於開銷總是需要權衡的。使用虛擬 DOM 所花費的開銷是否值得?

虛擬 DOM 的開銷

下載、解析並渲染一個 React 應用需要多長時間?

Rich Harris 本人對第一個問題給出瞭如是答案:

我們向用戶裝載的代碼太多了。與許多其他前端開發者一樣,我曾一直拒絕承認此事實,覺得給一個頁面加載 100kb 的 JavaScript 也無不妥 —— 少用一個 .jpg 就行了7

但接下來他說:

100kb 的 .js 和 100kb 的 .jpg 不可等而視之。不僅僅是網絡時間開銷會使應用的啟動性能變差,消耗在解析和評估腳本上的時間也會導致此效果,並且在這段時間內瀏覽器是完全無響應的。7

聽起來好怕怕呀,讓我們用 Google Chrome 瀏覽器的 Audit 工具測量一下。很幸運,藉助 realworld.io,我們能測出結果:

React-redux

[譯]為何Svelte殺不死React

Svelte

[譯]為何Svelte殺不死React

區別就是 0.15 秒 —— 毛毛雨啦。

那基準測試呢?Svelte 博客提到的基準測試表明,滑動 1000 行,React 需要 430.7 毫秒,而 Svelte 可以在 51.8 毫秒內做到。

但是這個度量標準並不可信,因為這種特殊操作是 React 做出協調假設導致的弱點 —— 這種場景在現實世界中很少見,同樣的基準測試表明,React 和 Svelte 在幾乎其他所有的案例中的差異都可以忽略不計。

[譯]為何Svelte殺不死React

現在,我們終於意識到,那些基準測試應該信一半扔一半。我們有窗口和虛擬化可以利用,一次渲染 1000 行真是個餿主意。說真的,你真這樣幹過嗎?

[譯]為何Svelte殺不死React

但 Svelte 的維護者聲稱虛擬 DOM 完全沒必要 —— 那何必浪費任何資源呢?什麼都不做最節約資源。

虛擬 DOM 的殺手鐗

虛擬 DOM 有一個殺手鐗,是 Svelte 無論如何都打不敗的。那就是把組件層級作為對象來處理的能力。

React 代碼:

const UnorderedList = ({children}) => (
<ul>
{
children.map((child, i) => <li key={i}>{child}</li>
)}
</ul>
)
const App = () => (
<UnorderedList>
<a href="http://example.com">Example</a>
<span>Example</span>
Text
</UnorderedList>
);

這個任務對 React 來說小菜一碟,但對 Svelte 來說難於登天。因為模板不是圖靈完備(Turing-complete)的,如果是,那它們就得需要虛擬 DOM。看起來似乎問題不大,但對我來說,已經是一個足夠正當的理由去給應用額外增加 0.15 秒到 0.25 秒的響應時間了。這正是我們需要虛擬 DOM 之處 —— 我們可能不需要用它來進行響應式的狀態更新、條件渲染或者列表渲染,但只要我們有了它,我們就能把組件層級作為完全動態的、可控的對象來處理。沒有這個功能,你不可能寫出一個真正的聲明式應用。

暫時性的限制(未來會修復)

還有其他幾個不使用 Svelte 的原因,它們可能會被修復。這需要社區成員大量的辛勤付出,但只要成本大於收益,修復就不會發生。

不支持 TypeScript

由於 Svelte 使用了模板,所以很難實現對 TypeScript 的完全支持,比如如 React 一般實現讓我們覺得十分便利的支持 prop 檢查的 TypeScript 支持。想解決這個問題,要麼在 Microsoft TypeScript 實現中做大幅更改(這不太可能,因為 Svelte 的影響力遠不如 React),要麼新建一個 fork 然後堅持不懈地維護。代碼生成也是個可選方案,但對元素層級中的每個細微的改變都運行一次代碼生成器,是個可怕的開發體驗。

不成熟

考慮互用性。想要用 npm 安裝炫酷的日曆工具並用在自己的應用中?在以前,只有你用的是(一個確定版本的)該工具適配的框架才行 —— 如果 cool-calendar-widget 是用 React 開發的,而你在用 Angular,那麼好吧,算你倒黴。但如果該工具的作者用了 Svelte 開發,那麼你可以隨意用哪種框架開發要使用該工具的應用。—— Rich Harris,Svelte 的維護者7

支持 React 的工具已經是應有盡有了 —— 十幾個 GraphQL 客戶端、超過 30 個表單狀態管理工具、上百個日期組件。

[譯]為何Svelte殺不死React

[譯]為何Svelte殺不死React

如果是在 2013 年,那 Svelte 這個功能可以算是殺手鐗了,但如今已經不值一提了。

前途光明?

雖然上面說了一些侷限,但我覺得 Svelte 實際上提出一了個前途無量的概念。沒錯,如果不犧牲靈活性和代碼可重用性,就無法通過模板完整地表達現代應用程序。但絕大多數的應用做的都只是條件渲染和列表渲染罷了。然後,我再說一遍,如果我只是在組件中使用 onChange={e => setState(e.target.value)} 並渲染一打 <div>,那我們為何還要去支持鍵盤事件、鼠標滾輪事件和內容可編輯功能呢?

實話實說,我並不相信 Svelte 能以當前這種形式打敗 React、橫掃世界。但如果有一個框架,它沒有任何特定的限制,卻能 100% 甩脫所有無用的部分,那就太酷了。要是能生成一些在運行時可用的有關其正確執行的構建時提示,那就更棒了。

說說可讀性

我們已經知道了,Svelte 的主打特性不是性能(這方面的益處微不足道),也沒那麼神奇(有些警告信息在 JavaScript 裡非常少見,理解起來非常吃力,而且缺少調試工具的支持簡直雪上加霜),更不具備互用性(放在 2014 年可能還算個角色,但如今我們的 React-NG-Vue 三大框架下已經應有盡有了)。那可讀性方面如何呢?

差距如此明顯,實在不多見 —— 在我的經驗裡,一個 React 組件要比對應的 Svelte 組件大 40% —— Rich Harris,Svelte 的維護者8

[譯]為何Svelte殺不死React

每段代碼你只會寫一次,但會讀許多次。我知道這是個人喜好的問題,也知道這是個有爭議的話題,但我覺得 JSX 和常規 JavaScript 流運算符要比其他任何形式的 {#blocks} 和指令都要通俗易懂。在 Vue 大紅大紫前,我就是它的忠實粉絲了。後來我時不時會被一些限制和意義不明的模板絆倒,於是開始全面使用 JSX —— 而因為 JSX 和 Vue 不是一個風格,我後來就轉向了 React。我不想再重蹈覆轍。


感謝閱讀! 😍

衷心希望你們喜歡本文。如果你有什麼高見,想要交流或者研討 —— 我全心全意地歡迎你在評論區留言!


引用:

1svelte.dev/
2github.com/sveltejs/rf…
3svelte.dev/blog/virtua…
4en.wikiquote.org/wiki/Donald…
5www.reddit.com/r/reactjs/c…
6svelte.dev/blog/virtua…
7svelte.dev/blog/framew…
8svelte.dev/blog/write-…

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章

從零到部署:用Vue和Express實現迷你全棧電商應用(三)

2020年最新整理Java面試題大全

Google開源的Python命令行庫:深入fire(二)

機器學習在高德用戶反饋信息處理中的實踐