新興前端框架 Svelte 從入門到原理

在這篇文章中,我們將會介紹 Svelte 框架的特性、優缺點和底層原理。

本文儘量不會涉及 Svelte 的語法,大家可以放心食用。因爲 Svelte 的語法極其簡單,而且官方教程學習曲線平緩 https://www.sveltejs.cn/,相信大家很快就會上手語法的,這裏就不做官網搬運工了。

前端領域是發展迅速,各種輪子層出不窮的行業。最近這些年,隨着三大框架ReactVueAngular版本逐漸穩定,前端技術棧的迭代似乎緩慢下來,React 16 版本推出了 Fiber, Vue 3.0 也已經在襁褓之中。

如果我們把目光拉伸到未來十年的視角,前端行業會出現哪些框架有可能會挑戰React或者Vue呢?我們認爲,嶄露頭角的 Svelte 應該是其中的選項之一。

Svelte 簡介

Svelte叫法是[Svelte], 本意是苗條纖瘦的,是一個新興熱門的前端框架。

在最新的《State of JS survey of 2020》中,它被預測爲未來十年可能取代 React 和 Vue 等其他框架的新興技術。如果你不確定自己是否該瞭解 Svelte,可以先看一下 Svelte 的一些發展趨勢。

開發者滿意度

從 2019 年開始, Svelte 出現在榜單中。剛剛過去的 2020 年,Svelte 在滿意度排行榜中超越了 react,躍升到了第一位。

開發者興趣度

在開發者興趣度方面,Svelte 蟬聯了第一。

市場佔有率

如果你在 19 年還沒有聽說過 Svelte,不用緊張,因爲svelte 當時仍是小衆的開發框架,在社區裏仍然沒有流行開來。

2020 年,Svelte 的市場佔有率從第 6 名躍升到第 4 名,僅次於 React、Angular、Vue 老牌前端框架。

svelte 作者——Rich Harris

Svelte作者是前端輪子哥 Rich Harris,同時也是 Rollup 的作者。Rich Harris 作者本人在介紹 Svelte 時,有一個非常精彩的演講《Rethinking reactivity》,油管連接:https://www.youtube.com/watch?v=AdNJ3fydeao&t=1900s,感興趣的同學不要錯過。

他設計 Svelte 的核心思想在於『通過靜態編譯減少框架運行時的代碼量』,也就是說,vue 和 react 這類傳統的框架,都必須引入運行時 (runtime) 代碼,用於虛擬 dom、diff 算法。Svelted 完全溶入 JavaScript,應用所有需要的運行時代碼都包含在bundle.js裏面了,除了引入這個組件本身,你不需要再額外引入一個運行代碼。

Svelte 優勢有哪些

我們先來看一下 Svelte 和 React,Vue 相比,有哪些優勢。

No Runtime —— 無運行時代碼

React 和 Vue 都是基於運行時的框架,當用戶在你的頁面進行各種操作改變組件的狀態時,框架的運行時會根據新的組件狀態(state)計算(diff)出哪些 DOM 節點需要被更新,從而更新視圖。

這就意味着,框架本身所依賴的代碼也會被打包到最終的構建產物中。這就不可避免增加了打包後的體積,有一部分的體積增加是不可避免的,那麼這部分體積大約是多少呢?請看下面的數據:

常用的框架中,最小的Vue都有58kReact更有97.5k。我們使用 React 開發一個小型組件,即使裏面的邏輯代碼很少,但是打包出來的 bundle size 輕輕鬆鬆都要 100k 起步。對於大型後臺管理系統來說,100k 不算什麼,但是對於特別注重用戶端加載性能的場景來說,一個組件 100k 多,還是太大了。

如果你特別在意打包出來的體積,Svelte 就是一個特別好的選擇。下面是Jacek Schae大神的統計,使用市面上主流的框架,來編寫同樣的 Realword 應用的體積:

從上圖的統計,Svelte 簡直是神奇!竟然只有 9.7 KB ! 果然魔法消失 UI 框架,無愧其名。

可以看出,Sveltebundle size大小是Vue的 1/4,是React的 1/20,體積上的優勢還是相當明顯的。

Less-Code ——寫更少的代碼

在寫svelte組件時,你就會發現,和 Vue 或 React 相比只需要更少的代碼。開發者的夢想之一,就是敲更少的代碼。因爲更少的代碼量,往往意味着有更好的語義性,也有更少的幾率寫出 bug。

下面的例子,可以看出SvelteReact的不同:

  1. React 的代碼
1const [count, setCount] = useState(0);
2
3function increment() {
4  setCount(count + 1);
5}
6
7
  1. Svelte 的代碼
1let count = 0;
2
3function increment() {
4  count += 1;
5}
6
7

雖然用上了 16 版本最新的 hooks,但是和svelte相比,代碼還是很冗餘。

React中,我們要麼使用useState鉤子,要麼使用setState設置狀態。而在Svelte中,可以直接使用賦值操作符更新狀態。

如果說上面的例子太簡單了,可以看下面的統計,分別使用 React 和 Svelte 實現下面的組件所需要的代碼行數

下面還是 Jacek Schae 老哥的統計,編寫同樣的 Realword 應用,各個框架所需要的行數

Vue 和 React 打了平手,Svelte 遙遙領先,可以少些 1000 行代碼耶!早日下班,指日可待。

Hight-Performance ——高性能

Virtual Dom已經是前端框架標配的今天, Svelte 聲稱自己是沒有Virtual Dom加持的, 怎麼還能保證高性能呢?

不急,慢慢看。

性能測評

Jacek Schae 在《A RealWorld Comparison of Front-End Frameworks with Benchmarks》中用主流的前端框架來編寫 RealWorld 應用,使用 Chrome 的 Lighthouse Audit 測試性能,得出數據是 Svelte 略遜於 Vue, 但好於 React。

是不是很驚奇?另外一個前端框架性能對比的項目也給出了同樣的答案:https://github.com/krausest/js-framework-benchmark。

爲什麼 Svelte 性能還不錯,至少沒有我們預期的那麼糟糕?我們接下來會在原理那一小結來介紹。

Svelte 劣勢

說完了 Svelte 的優勢,我們也要考慮到 Svelte 的劣勢。

和 Vue, React 框架的對比

在構建大型前端項目時,我們在選擇框架的時候就需要考慮更多的事情。Svelte 目前尚處在起步階段,對於大型項目必要的單元測試並沒有完整的方案。目前在大型應用中使用 Svelte , 需要謹慎評。

我們在用 Svelte 開發公司級別中大型項目時,也發現了其他的一些主要注意的點

但是 Svelte 需要從 0 開始 ” 抄 “ 出來一個 toast 或者彈窗組件出來,可能會帶來額外的開發量和做好加班的準備。

還需要注意的一點是,React / Vue 等框架自帶的runtime雖然會增加首屏加載的bundle.js,可是當項目變得越來越大的時候,框架的runtimebundle.js裏面佔據的比例也會越來越小,這個時候我們就得考慮一下是不是存在一個 Svelte 生成的代碼大於 React 和 Vue 生成的代碼的閾值了。

原理篇

Svelte 原理相對於 React 和 Vue 來說,相對比較簡單,大家可以放心的往下看。

首先,我們從一個問題出發:

Virtual Dom 真的高效嗎

Rich Harris 在設計 Svelte 的時候沒有采用 Virtual DOM 是因爲覺得 Virtual DOM Diff 的過程是非常低效的。

在他的一文《Virtual DOM is pure overhead》原文連接:https://www.sveltejs.cn/blog/virtual-dom-is-pure-overhead,感興趣的同學可以翻一下。

人們覺得 Virtual DOM 高效的一個理由,就是它不會直接操作原生的 DOM 節點。在瀏覽器當中,JavaScript 的運算在現代的引擎中非常快,但 DOM 本身是非常緩慢的東西。當你調用原生 DOM API 的時候,瀏覽器需要在 JavaScript 引擎的語境下去接觸原生的 DOM 的實現,這個過程有相當的性能損耗。

但其實 Virtual DOM 有時候會做很多無用功,這體現在很多組件會被 “無緣無故” 進行重渲染(re-render)。

比如說,下面的例子中,React 爲了更新掉 message 對應的 DOM 節點,需要做 n 多次遍歷,才能找到具體要更新哪些節點。

爲了解決這個問題,React 提供pureComponent,shouldComponentUpdateuseMemo,useCallback讓開發者來操心哪些subtree是需要重新渲染的,哪些是不需要重新渲染的。究其本質,是因爲 React 採用 jsx 語法過於靈活,不理解開發者寫出代碼所代表的意義,沒有辦法做出優化。

所以,React 爲了解決這個問題,在 v16.0 帶來了全新的 Fiber 架構,Fiber 思路是不減少渲染工作量,把渲染工作拆分成小任務思路是不減少渲染工作量。渲染過程中,留出時間來處理用戶響應,讓用戶感覺起來變快了。這樣會帶來額外的問題,不得不加載額外的代碼,用於處理複雜的運行時調度工作

那麼 Svelte 是如何解決這個問題的?

React 採用 jsx 語法本質不理解數據代表的意義,沒有辦法做出優化。Svelte 採用了Templates語法(類似於 Vue 的寫法),更加嚴格和具有語義性,可以在編譯的過程中就進行優化操作。

那麼,爲什麼Templates語法可以解決這個問題呢?

Template 帶來的優勢

關於 JSX 與 Templates ,可以看成是兩種不同的前端框架渲染機制,有興趣的同學可以翻一下尤雨溪的演講《在框架設計中尋求平衡》:https://www.bilibili.com/video/av80042358/。

一方面, JSX 的代表框架有 React 以及所有 react-like 庫,比如 preact、 stencil, infernal 等;另一方面, Templates 代表性的解決方案有 Vue、Svelte、 ember,各有優缺點。

JSX 優缺點

jsx 具有 JavaScript 的完整表現力,非常具有表現力,可以構建非常複雜的組件。

但是靈活的語法,也意味着引擎難以理解,無法預判開發者的用戶意圖,從而難以優化性能。你很可能會寫出下面的代碼:

在使用 JavaScript 的時候,編譯器不可能 hold 住所有可能發生的事情,因爲 JavaScript 太過於動態化。也有人對這塊做了很多嘗試,但從本質上來說很難提供安全的優化。

Template 優缺點

Template 模板是一種非常有約束的語言,你只能以某種方式去編寫模板。

例如,當你寫出這樣的代碼的時候,編譯器可以立刻明白:” 哦!這些 p 標籤的順序是不會變的,這個 id 是不會變的,這些 class 也不會變的,唯一會變的就是這個 “

在編譯時,編譯器對你的意圖可以做更多的預判,從而給它更多的空間去做執行優化。

左側 template 中,其他所有內容都是靜態的,只有 name 可能會發生改變。

右側 p 函數是編譯生成的最終的產物,是原生的 js 可以直接運行在瀏覽器裏,會在有髒數據時被調用。p 函數唯一做的事情就是,當 name 發生變更的時候,調用原生方法把 t1 這個原生 DOM 節點更新。這裏的 set_data 可不是 React 的 setState 或者小程序的 setData ,這裏的 set_data 就是封裝的原生的 javascript 操作 DOM 節點的方法。

如果我們仔細觀察上面的代碼,發現問題的關鍵在於 if 語句的判斷條件——changed.name, 表示有哪些變量被更新了,這些被更新的變量被稱爲髒數據。

任何一個現代前端框架,都需要記住哪些數據更新了,根據更新後的數據渲染出最新的 DOM

Svelte 記錄髒數據的方式:位掩碼(bitMask)

Svelte 使用位掩碼(bitMask) 的技術來跟蹤哪些值是髒的,即自組件最後一次更新以來,哪些數據發生了哪些更改。

位掩碼是一種將多個布爾值存儲在單個整數中的技術,一個比特位存放一個數據是否變化,一般1表示髒數據,0表示是乾淨數據。

用大白話來講,你有 A、B、C、D 四個值,那麼二進制0000 0001表示第一個值A發生了改變,0000 0010表示第二個值B發生了改變,0000 0100表示第三個值C發生了改變,0000 1000表示第四個D發生了改變。

這種表示法,可以最大程度的利用空間。爲啥這麼說呢?

比如說,十進制數字3就可以表示 A、B 是髒數據。先把十進制數字3, 轉變爲二進制0000 0011。從左邊數第一位、第二位是 1,意味着第一個值 A 和第二個值 B 是髒數據;其餘位都是 0,意味着其餘數據都是乾淨的。

JS 的限制

那麼,是不是用二進制比特位就可以記錄各種無窮無盡的變化了呢?

JS 的二進制有 31 位限制,number 類型最長是 32 位,減去 1 位用來存放符號。也就是說,如果 Svelte 採用二進制位存儲的方法,那麼只能存 31 個數據。

但肯定不能這樣,對吧?

Svelte 採用數組來存放,數組中一項是二進制31位的比特位。假如超出31個數據了,超出的部分放到數組中的下一項。

這個數組就是component.$.dirty數組,二進制的1位表示該對應的數據發生了變化,是髒數據,需要更新;二進制的0位表示該對應的數據沒有發生變化,是乾淨的

一探究竟component.$.dirty

上文中,我們說到component.$.dirty是數組,具體這個數組長什麼樣呢?

我們模擬一個 Svelte 組件,這個 Svelte 組件會修改 33 個數據。

我們打印出每一次make_dirty之後的component.$.dirty, 爲了方便演示,轉化爲二進制打印出來,如下面所示:

上面數組中的每一項中的每一個比特位,如果是 1,則代表着該數據是否是髒數據。如果是髒數據,則意味着更新。

當一個組件內,數據的個數,超出了31的數量限制,就數組新增一項來表示。

這樣,我們就可以通過component.$.dirty這個數組,清楚的知道有哪些數據發生了變化。那麼具體應該更新哪些 DOM 節點呢?

數據和 DOM 節點之間的對應關係

我們都知道, React 和 Vue 是通過 Virtual Dom 進行 diff 來算出來更新哪些 DOM 節點效率最高。Svelte 是在編譯時候,就記錄了數據 和 DOM 節點之間的對應關係,並且保存在 p 函數中。

這裏說的p 函數,就是 Svelte 的更新方法,本質上就是一大堆if判斷,邏輯非常簡單

1if ( A 數據變了 ) {
2  更新A對應的DOM節點
3}
4if ( B 數據變了 ) {
5  更新B對應的DOM節點
6}
7
8

爲了更加直觀的理解,我們模擬更新一下 33 個數據的組件,編譯得到的p 函數打印出來,如:

我們會發現,裏面就是一大堆if判斷,但是if判斷條件比較有意思,我們從上面摘取一行仔細觀察一下:

首先要注意,&不是邏輯與,而是按位與,會把兩邊數值轉爲二進制後進行比較,只有相同的二進制位都爲 1 纔會爲真。

這裏的if判斷條件是:拿compoenent.$.dirty[0](00000000000000000000000000000100) 和4(4 轉變爲二進制是0000 0100)做按位並操作。那麼我們可以思考一下了,這個按位並操作什麼時候會返回1呢?

4 是一個常量,轉變爲二進制是0000 0100, 第三位是1。那麼也就是,只有dirty[0]的二進制的第三位也是1時, 表達式纔會返回真。換句話來說,只有第三個數據是髒數據,纔會走入到這個if判斷中,執行set_data(t5, ctx[2]), 更新t5這個 DOM 節點。

當我們分析到這裏,已經看出了一些眉目,讓我們站在更高的一個層次去看待這 30 多行代碼:它們其實是保存了這 33 個變量 和 真實 DOM 節點之間的對應關係,哪些變量髒了,Svelte 會走入不同的if體內直接更新對應的 DOM 節點,而不需要複雜 Virtual DOM DIFF 算出更新哪些 DOM 節點

這 30 多行代碼,是 Svelte 編譯了我們寫的 Svelte 組件之後的產物,在 Svelte 編譯時,就已經分析好了,數據 和 DOM 節點之間的對應關係,在數據發生變化時,可以非常高效的來更新 DOM 節點。

Vue 曾經也是想採取這樣的思路,但是 Vue 覺得保存每一個髒數據太消耗內存了,於是沒有采用那麼細顆粒度,而是以組件級別的中等顆粒度,只監聽到組件的數據更新,組件內部再通過 DIFF 算法計算出更新哪些 DOM 節點。Svelte 採用了比特位的存儲方式,解決了保存髒數據會消耗內存的問題。

整體流程

上面就是 Svelte 最核心更新 DOM 機制,下面我們串起來整個的流程。

下面是非常簡單的一個 Svelte 組件,點擊<button>會觸發onClick事件,從而改變 name 變量。

上面代碼背後的整體流程如下圖所示,我們一步一步來看:

第一步,Svelte 會編譯我們的代碼,下圖中左邊是我們的源碼,右邊是 Svelte 編譯生成的。Svelte 在編譯過程中發現,『咦,這裏有一行代碼 name 被重新賦值了,我要插入一條make_dirty的調用』,於是當我們改寫 name 變量的時候,就會調用make_dirty方法把 name 記爲髒數據。

第二步,我們來看make_diry方法究竟做了什麼事情:

  1. 把對應數據的二進制改爲 1

  2. 把對應組件記爲髒組件,推入到 dirty_components 數組中

  3. 調用schedule_update()方法把flush方法推入到一幀中的微任務階段執行。因爲這樣既可以做頻繁更新 的截流,又避免了阻塞一幀中的 layout, repaint 階段的渲染。

schedule_update 方法其實就是一個promise.then()

一幀大概有 16ms, 大概會經歷 layout, repaint 的階段後,就可以開始執行微任務的回調了。

flush 方法做的事情也比較簡單,就是遍歷髒組件,依次調用update方法去更新對應的組件。

update方法除了執行一些生命週期的方法外,最核心的一行代碼是調用p方法,p方法我們已經在上文中介紹過很熟悉了。

p 方法的本質就是走入到不同的 if 判斷裏面,調用set_data原生的 javascript 方法更新對應的 DOM 節點。

至此,我們的頁面的 DOM 節點就已經更新好了。

上面的代碼均是剔除了分支邏輯的僞代碼。

Svelte 在處理子節點列表的時候,還是有優化的算法在的。比如說 [a,b,c,d] 變成 [d, a, b, c] ,但是隻是非常簡單的優化,簡單來說,是比較節點移動距離的絕對值,絕對值最小的節點被移動。

所以,嚴格意義上來說,Svelte 並不是 100% 無運行時,還是會引入額外的算法邏輯,只是量很少罷了。

總結

一個前端框架,不管是vue還是react更新了數據之後,需要考慮更新哪個 dom 節點,也就是,需要知道,髒數據和待更新的真實 dom 之間的映射。vue, react 是通過 virtualDom 來 diff 計算出更新哪些 dom 節點更划算,而sveltedom 是把數據和真實 dom 之間的映射關係,在編譯的時候就通過 AST 等算出來,保存在p函數中。

Svelte 作爲新興的前端框架,採用了和 React, Vue 不同的設計思路,其獨特的特性在某些場景下還是很值得嘗試的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Lf7diVKMmqqLiglUON4CnA