比較前端框架ReactJs、SolidJS、Svelte和Lit底層邏輯 - Smashing

banq發表於2022-03-21

選擇了四個框架來研究:React是當今占主導地位的框架,以及三個聲稱與React不同的新競爭者。
  • React“React 讓建立互動式 UI 變得輕鬆。宣告式檢視使您的程式碼更可預測且更易於除錯。”
  • SolidJS“Solid 遵循與 React 相同的理念......但是它有一個完全不同的實現,它放棄了使用虛擬 DOM。”
  • Svelte“Svelte 是一種構建使用者介面的全新方法……在構建應用程式時發生的編譯步驟。Svelte 沒有使用虛擬 DOM 差異等技術,而是編寫了在您的應用程式狀態發生變化時透過手術更新 DOM 的程式碼。”
  • Lit“在 Web Components 標準之上構建,Lit 僅新增了……反應性、宣告性模板和一些深思熟慮的功能。”


總結一下這些框架對它們的區別的看法:
  • React 使用宣告式檢視讓構建 UI 變得更容易。
  • SolidJS 遵循 React 的理念,但使用了不同的技術。
  • Svelte 對 UI 使用compile-time方法。
  • Lit 使用現有標準,並新增了一些輕量級功能。

框架本身提到了宣告性、反應性和虛擬 DOM 等詞。讓我們深入瞭解這些含義:
 

宣告式程式設計
宣告式程式設計是一種正規化,在這種正規化中,邏輯被定義,而沒有指定控制流。
我們描述需要的結果是什麼,而不是哪些步驟能讓我們達到目的。

在宣告式框架的早期,大約在2010年,DOM的API更加赤裸裸和冗長,用命令式的JavaScript編寫Web應用程式需要大量的模板程式碼。這時,"模型-檢視-檢視模型"(MVVM)的概念開始盛行,當時具有劃時代意義的Knockout和AngularJS框架,提供了一個JavaScript宣告層,在庫內處理這種複雜性。

今天,MVVM並不是一個廣泛使用的術語,它在某種程度上是舊術語 "資料繫結 "的變種。
  

資料繫結
資料繫結是一種宣告性的方式來表達資料如何在模型和使用者介面之間進行同步。

所有流行的UI框架都提供了某種形式的資料繫結,它們的教程都以資料繫結的例子開始。

這裡是JSX(SolidJS和React)中的資料繫結。

function HelloWorld() {
 const name = "Solid or React";

 return (
     <div>Hello {name}!</div>
 )
}


Lit中Data-binding資料繫結 :

class HelloWorld extends LitElement {
 @property()
 name = 'lit';

 render() {
   return html`<p>Hello ${this.name}!</p>`;
 }
}


Svelte中資料繫結:

<script>
  let name = 'world';
</script>

<h1>Hello {name}!</h1>


  

反應式
Reactivity是一種宣告性的方式來表達資料改變的傳播。
當我們有一種宣告性地表達資料繫結的方式時,我們需要一種有效的方式來讓框架傳播資料改變。

  • React引擎將渲染的結果與之前的結果進行比較,並將差異應用於DOM本身。這種處理變化傳播的方式被稱為虛擬 DOM。
  • 在SolidJS中,這是以其儲存和內建元素更明確地完成的。例如,"Show"元素將跟蹤內部的變化,而不是虛擬 DOM。
  • 在Svelte中,"反應式 "程式碼被生成。Svelte知道哪些事件會導致變化,它會生成直接的程式碼,在事件和DOM變化之間劃清界限。
  • 在Lit中,反應性是透過元素屬性完成的,基本上是依靠HTML自定義元素的內建反應性。

 

邏輯
當一個框架為資料繫結提供了一個宣告性的介面,並實現了反應性,它也需要提供一些方法來表達一些傳統上必須寫的邏輯。邏輯的基本構件是 "if "和 "for",所有主要的框架都提供了這些構件的一些表達。

  • 條件式conditional #

除了繫結基本資料如數字和字串,每個框架都提供了一個 "conditional "語法。
在React中,它看起來像這樣。

const [hasError, setHasError] = useState(false);  
return hasError ? <label>Message</label> : null;
…
setHasError(true);


SolidJS提供了一個內建的條件元件,Show:

<Show when={state.error}>
  <label>Message</label>
</Show>


Svelte提供了if指令

{if state.error}
  <label>Message</label>
{/if}


在Lit中,你會在渲染函式中使用明確的三元操作:

render() {
 return this.error ? html`<label>Message</label>`: null;
}

 

列表 
另一個常見的框架基元是列表處理。列表是使用者介面的一個關鍵部分--聯絡人列表、通知等--為了有效地工作,它們需要是反應性的,而不是在一個資料項發生變化時更新整個列表。
在React中,列表處理看起來像這樣:

contacts.map((contact, index) =>
 <li key={index}>
   {contact.name}
 </li>)


React使用特殊的key屬性來區分列表項,它確保整個列表不會在每次渲染時被替換。

在SolidJS中,使用了for和index內建元素。

<For each={state.contacts}>
  {contact => <DIV>{contact.name}</DIV> }
</For>


在內部,SolidJS使用它自己的儲存與for和index相結合,以決定當專案發生變化時要更新哪些元素。
它比React更明確,使我們能夠避免虛擬DOM的複雜性。

Svelte使用each指令,根據其更新器進行轉譯:

{each contacts as contact}
  <div>{contact.name}</div>
{/each}


Lit提供了一個重複函式,它的工作原理類似於React的基於鍵的列表對映:

repeat(contacts, contact => contact.id,
    (contact, index) => html`<div>${contact.name}</div>`

 

元件模型 
有一件事超出了本文的範圍,那就是不同框架中的元件模型,以及如何使用自定義HTML元素來處理它。
 

成本
框架提供了宣告性的資料繫結,控制流原語(條件和列表),以及傳播變化的反應式機制。
它們還提供了其他重要的東西,比如重用元件的方法,但這是另一篇文章的主題。
 

框架有用嗎?是的。它們給了我們所有這些方便的功能。但這是一個正確的問題嗎?使用框架是有代價的。讓我們看看這些代價是什麼。
 

捆綁包大小 
在檢視bundle size時,我喜歡看非Gzip的minified size。這是與JavaScript執行的CPU成本最相關的大小。

  • ReactDOM 大約是 120 KB。
  • SolidJS大約是18KB。
  • Lit大約是16KB。
  • Svelte約為2KB,但生成的程式碼大小不一。

似乎今天的框架在保持捆綁大小方面做得比React更好。虛擬DOM需要大量的JavaScript。
 

構建
不知為何,我們習慣於 "構建 "我們的網路應用。如果不設定Node.js和Webpack這樣的捆綁器,不處理Babel-TypeScript啟動包中最近的一些配置變化,就不可能啟動一個前端專案,以及所有這些事情。

框架的表現力越強,捆綁規模越小,構建工具和轉譯時間的負擔就越大。

Svelte聲稱,虛擬DOM是純粹的開銷。
我同意,但也許 "構建"(如Svelte和SolidJS)和自定義客戶端模板引擎(如Lit)也是純粹的開銷,是不同型別的?
 

除錯
隨著構建和轉譯的進行,會產生一種不同的代價。

當我們使用或除錯網路應用時,我們看到的程式碼與我們寫的完全不同。我們現在依靠不同質量的特殊除錯工具來反向設計網站上發生的事情,並將其與我們自己程式碼中的錯誤聯絡起來。

  • 在React中,呼叫棧從來不是 "你的"--React為你處理排程。當沒有bug的時候,這很好用。但如果你想找出無限迴圈重現的原因,你就會陷入痛苦的境地。
  • 在Svelte中,庫本身的體積很小,但你要運送和除錯一大堆神秘的生成程式碼,這些程式碼是Svelte對反應性的實現,根據你的應用需求定製。
  • 對於Lit來說,它不太需要構建,但為了有效地除錯它,你必須瞭解它的模板引擎。這可能是我對框架持懷疑態度的最大原因。


當你尋找自定義的宣告式解決方案時,你最終會面臨更痛苦的命令式除錯。本文中的例子使用Typescript來規範API,但程式碼本身並不要求轉譯。
 

升級
在本文中,我看了四個框架,但框架多得數不清(AngularJS、Ember.js和Vue.js,僅舉幾例)。你能指望框架、它的開發者、它的思想份額和它的生態系統在發展中為你工作嗎?

有一件事比修復你自己的錯誤更令人沮喪,那就是必須為框架的錯誤找到變通辦法。而比框架bug更令人沮喪的一件事是,當你把框架升級到新版本而不修改你的程式碼時,就會出現bug。

誠然,這個問題也存在於瀏覽器中,但當它發生時,它發生在每個人身上,而且在大多數情況下,修復或公佈的變通方法是迫在眉睫的。另外,本文中的大多數模式都是基於成熟的網路平臺API;並不總是需要走在流血的邊緣。
  

總結
我們研究了使用框架的不同好處和成本,從他們試圖解決的核心問題的角度出發,重點關注宣告式程式設計、資料繫結、反應性、列表和條件。
Web 平臺已經提供了開箱即用的宣告式程式設計機制:HTML 和 CSS。這種機制是成熟的、經過良好測試的、流行的、廣泛使用的和記錄在案的。但是,它沒有提供明確的資料繫結、條件渲染和列表同步的內建概念,並且反應性是跨多個平臺功能傳播的微妙細節。
 

保持DOM樹的穩定實現反應性
在 ReactJS 和 SolidJS 中,我們建立了轉換為命令式程式碼的宣告性程式碼,將標籤新增到 DOM 或刪除它。在 Svelte 中,會生成該程式碼。
但是我們可以使用 CSS 來隱藏和顯示錯誤標籤:

<style>
    label.error { display: none; }
    .app.has-error label.error {display: block; }
</style>
<label class="error">Message</label>

<script>
   app.classList.toggle('has-error', true);
</script>


在這種情況下,反應是在瀏覽器中處理的——應用程式的類更改會傳播到其後代,直到瀏覽器中的內部機制決定是否呈現標籤。
這種技術有幾個優點:
  • 捆綁包大小為零。
  • 構建步驟為零。
  • 更改傳播在本機瀏覽器程式碼中經過最佳化和良好測試,並避免了不必要的昂貴 DOM 操作,如append和remove.
  • 選擇器selector是穩定的。在這種情況下,您可以指望標籤元素在那裡。您可以對其應用動畫,而無需依賴諸如“過渡組”之類的複雜結構。您可以在 JavaScript 中儲存對它的引用。
  • 如果標籤顯示或隱藏,您可以在開發人員工具的樣式皮膚中看到原因,該皮膚向您顯示整個級聯,最終在標籤中顯示(或隱藏)的規則鏈。

 

面向表單的“資料繫結”
在 JavaScript 繁重的單頁應用程式 (SPA) 時代之前,表單是建立包含使用者輸入的 Web 應用程式的主要方式。傳統上,使用者填寫表單並單擊“提交”按鈕,伺服器端程式碼將處理響應。表單是資料繫結和互動的多頁應用程式版本。
在上一節的錯誤標籤示例中,我們展示瞭如何反應性地顯示和隱藏錯誤訊息。這就是我們在 React 中更新錯誤訊息文字的方式(在 SolidJS 中也是如此):

const [errorMessage, setErrorMessage] = useState(null);
return <label className="error">{errorMessage}</label>


當我們擁有穩定的 DOM 和穩定的樹形表單和表單元素時,我們可以執行以下程式碼替代使用這些框架:

<form name="contactForm">
  <fieldset name="email">
     <output name="error"></output>
  </fieldset>
</form>

<script>
  function setErrorMessage(message) {
  document.forms.contactForm.elements.email.elements.error.value = message;
  }
</script>

看起來非常冗長,但它也非常穩定、直接且非常高效。
  

輸入資料的表單 
正確使用表格,有一個簡潔的替代方案:

<form name="contactForm">
  <input name="id" type="hidden" value="136" />
  <input name="email" type="email"/>
  <input name="name" type="string" />
  <input name="subscriber" type="checkbox" />
</form>

<script>
   updateContact(Object.fromEntries(
       new FormData(document.forms.contactForm));
</script>

透過使用隱藏輸入和有用的FormData類,我們可以在 DOM 輸入和 JavaScript 函式之間無縫轉換值。
 

結合表單和反應性 
透過結合表單的高效能選擇器穩定性和 CSS 響應性,我們可以實現更復雜的 UI 邏輯:

<form name="contactForm">
  <input name="showErrors" type="checkbox" hidden />
  <fieldset name="names">
     <input name="name" />
     <output name="error"></output>
  </fieldset>
  <fieldset name="emails">
     <input name="email" />
     <output name="error"></output>
  </fieldset>
</form>

<script>
  function setErrorMessage(section, message) {
  document.forms.contactForm.elements[section].elements.error.value = message;
  }
  function setShowErrors(show) {
  document.forms.contactForm.elements.showErrors.checked = show;
  }
</script>

<style>
   input[name="showErrors"]:not(:checked) ~ * output[name="error"] {
      display: none;
   }
</style>

請注意,在此示例中,沒有使用類——我們從表單的資料中開發 DOM 的行為和樣式,而不是透過手動更改元素類。

 

表單的優勢 :
  • 與層疊式一樣,表單是內建於網路平臺的,其大部分功能是穩定的。這意味著更少的JavaScript,更少的框架版本不匹配,而且沒有 "構建"。
  • 預設情況下,表單是可訪問的。如果你的應用程式正確地使用表單,就不需要ARIA屬性、"可訪問性外掛 "和最後一分鐘的審計。表單適合於鍵盤導航、螢幕閱讀器和其他輔助技術。
  • 表單具有內建的輸入驗證功能:透過regex模式驗證,在CSS中對無效和有效表單的反應性,處理必填與可選,等等。你不需要為了享受這些功能而使某些東西看起來像一個表單。
  • 表單的提交事件是非常有用的。例如,它允許在沒有提交按鈕的情況下捕獲 "Enter "鍵,並允許透過提交者屬性來區分多個提交按鈕(正如我們將在後面的TODO例子中看到的)。
  • 預設情況下,元素與它們所包含的表單相關聯,但也可以使用表單屬性與文件中的任何其他表單相關聯。這使我們能夠在不對DOM樹產生依賴的情況下玩轉表單關聯。
  • 使用穩定的選擇器有助於實現UI測試自動化。我們可以使用巢狀的API作為一種穩定的方式來鉤住DOM,而不管它的佈局和層次結構如何。表單>(欄位集)>元素的層次結構可以作為你的文件的互動骨架。

 

列表項的 HTML 模板元素
當我們使用一個template元素時,我們可以避免建立元素並在 JavaScript 中填充它們的所有樣板程式碼。

<ul id="names">
  <template>
   <li><label class="name" /></li>
  </template>
</ul>
<script>
  function addName(name) {
    const list = document.querySelector('names');
    const item = list.querySelector('template').content.cloneNode(true).firstElementChild;
    item.querySelector('label').innerText = name;
    list.appendChild(item);
  }
</script>


透過使用template列表項的元素,我們可以在原始 HTML 中看到列表項——它不是使用 JSX 或其他語言“渲染”的。您的 HTML 檔案現在包含應用程式的所有HTML — 靜態部分是呈現的 DOM 的一部分,而動態部分在模板中表示,準備好在時機成熟時克隆並附加到文件中。

TodoMVC是用於展示不同框架的 TODO 列表的應用程式規範。TodoMVC 模板帶有現成的 HTML 和 CSS,可幫助您專注於框架。
您可以在 GitHub 儲存庫中使用結果,並且可以使用[url=https://github.com/noamr/todomvc-app-template]完整的原始碼[/url]。

最佳實踐

  • 保持DOM樹的穩定。它會啟動了一個連鎖反應,使事情變得簡單。
  • 如果可以的話,依靠CSS的反應性而不是JavaScript。
  • 使用表單元素作為表示互動資料的主要方式。
  • 使用HTML template元素而不是JavaScript生成的模板。
  • 使用雙向的變化流作為你的模型的介面。


 原文點選標題

相關文章