新興前端框架 Svelte 從入門到原理
在這篇文章中,我們將會介紹 Svelte 框架的特性、優(yōu)缺點和底層原理。
本文盡量不會涉及 Svelte 的語法,大家可以放心食用。因為 Svelte 的語法極其簡單,而且官方教程學習曲線平緩https://www.sveltejs.cn/,相信大家很快就會上手語法的,這里就不做官網(wǎng)搬運工了。
前端領(lǐng)域是發(fā)展迅速,各種輪子層出不窮的行業(yè)。最近這些年,隨著三大框架React、Vue、Angular版本逐漸穩(wěn)定,前端技術(shù)棧的迭代似乎緩慢下來,React 16版本推出了 Fiber, Vue 3.0 也已經(jīng)在襁褓之中。
如果我們把目光拉伸到未來十年的視角,前端行業(yè)會出現(xiàn)哪些框架有可能會挑戰(zhàn)React或者Vue呢?我們認為,嶄露頭角的 Svelte 應(yīng)該是其中的選項之一。
Svelte 簡介
Svelte叫法是[Svelte], 本意是苗條纖瘦的,是一個新興熱門的前端框架。

在最新的《State of JS survey of 2020》中,它被預(yù)測為未來十年可能取代React和Vue等其他框架的新興技術(shù)。如果你不確定自己是否該了解 Svelte,可以先看一下 Svelte 的一些發(fā)展趨勢。
開發(fā)者滿意度
從2019年開始, Svelte出現(xiàn)在榜單中。剛剛過去的2020年,Svelte在滿意度排行榜中超越了react,躍升到了第一位。

開發(fā)者興趣度
在開發(fā)者興趣度方面,Svelte 蟬聯(lián)了第一。

市場占有率
如果你在19年還沒有聽說過Svelte,不用緊張,因為svelte 當時仍是小眾的開發(fā)框架,在社區(qū)里仍然沒有流行開來。

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,感興趣的同學不要錯過。
他設(shè)計 Svelte 的核心思想在于『通過靜態(tài)編譯減少框架運行時的代碼量』,也就是說,vue 和 react 這類傳統(tǒng)的框架,都必須引入運行時 (runtime) 代碼,用于虛擬dom、diff 算法。Svelted完全溶入JavaScript,應(yīng)用所有需要的運行時代碼都包含在bundle.js里面了,除了引入這個組件本身,你不需要再額外引入一個運行代碼。
Svelte 優(yōu)勢有哪些
我們先來看一下 Svelte 和React,Vue 相比,有哪些優(yōu)勢。
No Runtime —— 無運行時代碼
React 和 Vue 都是基于運行時的框架,當用戶在你的頁面進行各種操作改變組件的狀態(tài)時,框架的運行時會根據(jù)新的組件狀態(tài)(state)計算(diff)出哪些DOM節(jié)點需要被更新,從而更新視圖。
這就意味著,框架本身所依賴的代碼也會被打包到最終的構(gòu)建產(chǎn)物中。這就不可避免增加了打包后的體積,有一部分的體積增加是不可避免的,那么這部分體積大約是多少呢?請看下面的數(shù)據(jù):

常用的框架中,最小的Vue都有58k,React更有97.5k。我們使用React開發(fā)一個小型組件,即使里面的邏輯代碼很少,但是打包出來的bundle size輕輕松松都要100k起步。對于大型后臺管理系統(tǒng)來說,100k 不算什么,但是對于特別注重用戶端加載性能的場景來說,一個組件100k 多,還是太大了。
如果你特別在意打包出來的體積,Svelte 就是一個特別好的選擇。下面是Jacek Schae大神的統(tǒng)計,使用市面上主流的框架,來編寫同樣的Realword 應(yīng)用的體積:

從上圖的統(tǒng)計,Svelte簡直是神奇!竟然只有 9.7 KB ! 果然魔法消失 UI 框架,無愧其名。
可以看出,Svelte的bundle size大小是Vue的1/4,是React的1/20,體積上的優(yōu)勢還是相當明顯的。
Less-Code ——寫更少的代碼
在寫svelte組件時,你就會發(fā)現(xiàn),和 Vue 或 React 相比只需要更少的代碼。開發(fā)者的夢想之一,就是敲更少的代碼。因為更少的代碼量,往往意味著有更好的語義性,也有更少的幾率寫出bug。
下面的例子,可以看出Svelte和React的不同:
React的代碼
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
Svelte的代碼
let count = 0;
function increment() {
count += 1;
}
雖然用上了16版本最新的 hooks,但是和svelte相比,代碼還是很冗余。
在React中,我們要么使用useState鉤子,要么使用setState設(shè)置狀態(tài)。而在Svelte中,可以直接使用賦值操作符更新狀態(tài)。
如果說上面的例子太簡單了,可以看下面的統(tǒng)計,分別使用 React 和 Svelte 實現(xiàn)下面的組件所需要的代碼行數(shù)

下面還是 Jacek Schae 老哥的統(tǒng)計,編寫同樣的Realword 應(yīng)用,各個框架所需要的行數(shù)

Vue 和 React 打了平手,Svelte 遙遙領(lǐng)先,可以少些 1000 行代碼耶!早日下班,指日可待。
Hight-Performance ——高性能
在Virtual Dom已經(jīng)是前端框架標配的今天, Svelte 聲稱自己是沒有Virtual Dom加持的, 怎么還能保證高性能呢?
不急,慢慢看。
性能測評
Jacek Schae 在《A RealWorld Comparison of Front-End Frameworks with Benchmarks》中用主流的前端框架來編寫 RealWorld 應(yīng)用,使用 Chrome 的Lighthouse Audit測試性能,得出數(shù)據(jù)是Svelte 略遜于Vue, 但好于 React。

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

為什么 Svelte 性能還不錯,至少沒有我們預(yù)期的那么糟糕?我們接下來會在原理那一小結(jié)來介紹。
Svelte 劣勢
說完了 Svelte 的優(yōu)勢,我們也要考慮到 Svelte 的劣勢。
和Vue, React框架的對比
在構(gòu)建大型前端項目時,我們在選擇框架的時候就需要考慮更多的事情。Svelte 目前尚處在起步階段,對于大型項目必要的單元測試并沒有完整的方案。目前在大型應(yīng)用中使用 Svelte , 需要謹慎評。
| 類目 | Svelte | Vue | React |
|---|---|---|---|
| UI 組件庫 | Material design ( 坦率的說,不好用 ) | Element UI / AntD | AntD / Material design |
| 狀態(tài)管理 | 官網(wǎng)自帶 | Vuex | Redux/MobX |
| 路由 | Svelte-router | Vue-router | React-router |
| 服務(wù)端渲染 | 支持 | 支持 | 支持 |
| 測試工具 | 官方網(wǎng)站沒有相關(guān)內(nèi)容 | @vue/test-utils | Jest |
我們在用 Svelte 開發(fā)公司級別中大型項目時,也發(fā)現(xiàn)了其他的一些主要注意的點
沒有像AntD那樣成熟的UI庫。比如說需求方想加一個toast提示,或者彈窗,pm:”很簡單的,不用出UI稿,就直接用之前的樣式好啦~“
但是 Svelte 需要從0開始 ”抄“ 出來一個toast或者彈窗組件出來,可能會帶來額外的開發(fā)量和做好加班的準備。
Svelte 原生不支持預(yù)處理器,比如說
less/scss,需要自己單獨的配置 webpack loader。Svelte 原生腳手架沒有目錄劃分
暫時不支持typescript,雖然官方說了會支持, 但是不知道什么時候.
還需要注意的一點是,React / Vue等框架自帶的runtime雖然會增加首屏加載的bundle.js,可是當項目變得越來越大的時候,框架的runtime在bundle.js里面占據(jù)的比例也會越來越小,這個時候我們就得考慮一下是不是存在一個Svelte生成的代碼大于React和Vue生成的代碼的閾值了。
原理篇
Svelte 原理相對于 React 和 Vue 來說,相對比較簡單,大家可以放心的往下看。
首先,我們從一個問題出發(fā):
Virtual Dom 真的高效嗎
Rich Harris 在設(shè)計 Svelte 的時候沒有采用 Virtual DOM 是因為覺得Virtual DOM Diff 的過程是非常低效的。
在他的一文《Virtual DOM is pure overhead》原文連接:https://www.sveltejs.cn/blog/virtual-dom-is-pure-overhead,感興趣的同學可以翻一下。
人們覺得 Virtual DOM高效的一個理由,就是它不會直接操作原生的DOM節(jié)點。在瀏覽器當中,JavaScript的運算在現(xiàn)代的引擎中非常快,但DOM本身是非常緩慢的東西。當你調(diào)用原生DOM API的時候,瀏覽器需要在JavaScript引擎的語境下去接觸原生的DOM的實現(xiàn),這個過程有相當?shù)男阅軗p耗。
但其實 Virtual DOM 有時候會做很多無用功,這體現(xiàn)在很多組件會被“無緣無故”進行重渲染(re-render)。
比如說,下面的例子中,React 為了更新掉message 對應(yīng)的DOM 節(jié)點,需要做n多次遍歷,才能找到具體要更新哪些節(jié)點。

為了解決這個問題,React 提供pureComponent,shouldComponentUpdate,useMemo,useCallback讓開發(fā)者來操心哪些subtree是需要重新渲染的,哪些是不需要重新渲染的。究其本質(zhì),是因為 React 采用 jsx 語法過于靈活,不理解開發(fā)者寫出代碼所代表的意義,沒有辦法做出優(yōu)化。
所以,React 為了解決這個問題,在 v16.0 帶來了全新的 Fiber 架構(gòu),F(xiàn)iber 思路是不減少渲染工作量,把渲染工作拆分成小任務(wù)思路是不減少渲染工作量。渲染過程中,留出時間來處理用戶響應(yīng),讓用戶感覺起來變快了。這樣會帶來額外的問題,不得不加載額外的代碼,用于處理復(fù)雜的運行時調(diào)度工作
那么 Svelte 是如何解決這個問題的?
React 采用 jsx 語法本質(zhì)不理解數(shù)據(jù)代表的意義,沒有辦法做出優(yōu)化。Svelte 采用了Templates語法(類似于 Vue 的寫法),更加嚴格和具有語義性,可以在編譯的過程中就進行優(yōu)化操作。
那么,為什么Templates語法可以解決這個問題呢?
Template 帶來的優(yōu)勢
關(guān)于 JSX 與 Templates ,可以看成是兩種不同的前端框架渲染機制,有興趣的同學可以翻一下尤雨溪的演講《在框架設(shè)計中尋求平衡》:https://www.bilibili.com/video/av80042358/。
一方面, JSX 的代表框架有 React 以及所有 react-like 庫,比如 preact、 stencil, infernal 等;另一方面, Templates 代表性的解決方案有 Vue、Svelte、 ember,各有優(yōu)缺點。

JSX 優(yōu)缺點
jsx 具有 JavaScript 的完整表現(xiàn)力,非常具有表現(xiàn)力,可以構(gòu)建非常復(fù)雜的組件。
但是靈活的語法,也意味著引擎難以理解,無法預(yù)判開發(fā)者的用戶意圖,從而難以優(yōu)化性能。你很可能會寫出下面的代碼:

在使用 JavaScript 的時候,編譯器不可能hold住所有可能發(fā)生的事情,因為 JavaScript 太過于動態(tài)化。也有人對這塊做了很多嘗試,但從本質(zhì)上來說很難提供安全的優(yōu)化。
Template優(yōu)缺點
Template模板是一種非常有約束的語言,你只能以某種方式去編寫模板。
例如,當你寫出這樣的代碼的時候,編譯器可以立刻明白:”哦!這些 p 標簽的順序是不會變的,這個 id 是不會變的,這些 class 也不會變的,唯一會變的就是這個“。

在編譯時,編譯器對你的意圖可以做更多的預(yù)判,從而給它更多的空間去做執(zhí)行優(yōu)化。

左側(cè) template 中,其他所有內(nèi)容都是靜態(tài)的,只有 name 可能會發(fā)生改變。
右側(cè) p 函數(shù)是編譯生成的最終的產(chǎn)物,是原生的js可以直接運行在瀏覽器里,會在有臟數(shù)據(jù)時被調(diào)用。p 函數(shù)唯一做的事情就是,當 name 發(fā)生變更的時候,調(diào)用原生方法把 t1 這個原生DOM節(jié)點更新。這里的 set_data 可不是 React 的 setState 或者小程序的 setData ,這里的set_data 就是封裝的原生的 javascript 操作DOM 節(jié)點的方法。

如果我們仔細觀察上面的代碼,發(fā)現(xiàn)問題的關(guān)鍵在于 if 語句的判斷條件——changed.name, 表示有哪些變量被更新了,這些被更新的變量被稱為臟數(shù)據(jù)。
任何一個現(xiàn)代前端框架,都需要記住哪些數(shù)據(jù)更新了,根據(jù)更新后的數(shù)據(jù)渲染出最新的DOM
Svelte 記錄臟數(shù)據(jù)的方式:位掩碼(bitMask)
Svelte使用位掩碼(bitMask) 的技術(shù)來跟蹤哪些值是臟的,即自組件最后一次更新以來,哪些數(shù)據(jù)發(fā)生了哪些更改。
位掩碼是一種將多個布爾值存儲在單個整數(shù)中的技術(shù),一個比特位存放一個數(shù)據(jù)是否變化,一般1表示臟數(shù)據(jù),0表示是干凈數(shù)據(jù)。

用大白話來講,你有A、B、C、D 四個值,那么二進制0000 0001表示第一個值A發(fā)生了改變,0000 0010表示第二個值B發(fā)生了改變,0000 0100表示第三個值C發(fā)生了改變,0000 1000表示第四個D發(fā)生了改變。
這種表示法,可以最大程度的利用空間。為啥這么說呢?
比如說,十進制數(shù)字3就可以表示 A、B是臟數(shù)據(jù)。先把十進制數(shù)字3, 轉(zhuǎn)變?yōu)槎M制0000 0011。從左邊數(shù)第一位、第二位是1,意味著第一個值A(chǔ) 和第二個值B是臟數(shù)據(jù);其余位都是0,意味著其余數(shù)據(jù)都是干凈的。

JS 的限制
那么,是不是用二進制比特位就可以記錄各種無窮無盡的變化了呢?
JS 的二進制有31位限制,number 類型最長是32位,減去1位用來存放符號。也就是說,如果 Svelte 采用二進制位存儲的方法,那么只能存 31個數(shù)據(jù)。
但肯定不能這樣,對吧?
Svelte 采用數(shù)組來存放,數(shù)組中一項是二進制31位的比特位。假如超出31個數(shù)據(jù)了,超出的部分放到數(shù)組中的下一項。
這個數(shù)組就是component.$.dirty數(shù)組,二進制的1位表示該對應(yīng)的數(shù)據(jù)發(fā)生了變化,是臟數(shù)據(jù),需要更新;二進制的0位表示該對應(yīng)的數(shù)據(jù)沒有發(fā)生變化,是干凈的。
一探究竟component.$.dirty
上文中,我們說到component.$.dirty是數(shù)組,具體這個數(shù)組長什么樣呢?
我們模擬一個 Svelte 組件,這個 Svelte 組件會修改33個數(shù)據(jù)。
我們打印出每一次make_dirty之后的component.$.dirty, 為了方便演示,轉(zhuǎn)化為二進制打印出來,如下面所示:

上面數(shù)組中的每一項中的每一個比特位,如果是1,則代表著該數(shù)據(jù)是否是臟數(shù)據(jù)。如果是臟數(shù)據(jù),則意味著更新。
第一行
["0000000000000000000000000000001", "0000000000000000000000000000000"], 表示第一個數(shù)據(jù)臟了,需要更新第一個數(shù)據(jù)對應(yīng)的dom節(jié)點第二行
["0000000000000000000000000000011", "0000000000000000000000000000000"], 表示第一個、第二個數(shù)據(jù)都臟了,需要更新第一個,第二個數(shù)據(jù)對應(yīng)的dom節(jié)點。……
當一個組件內(nèi),數(shù)據(jù)的個數(shù),超出了31的數(shù)量限制,就數(shù)組新增一項來表示。
這樣,我們就可以通過component.$.dirty這個數(shù)組,清楚的知道有哪些數(shù)據(jù)發(fā)生了變化。那么具體應(yīng)該更新哪些DOM 節(jié)點呢?
數(shù)據(jù)和DOM節(jié)點之間的對應(yīng)關(guān)系
我們都知道, React 和 Vue 是通過 Virtual Dom 進行 diff 來算出來更新哪些 DOM 節(jié)點效率最高。Svelte 是在編譯時候,就記錄了數(shù)據(jù) 和 DOM 節(jié)點之間的對應(yīng)關(guān)系,并且保存在 p 函數(shù)中。

這里說的p 函數(shù),就是 Svelte 的更新方法,本質(zhì)上就是一大堆if判斷,邏輯非常簡單
if ( A 數(shù)據(jù)變了 ) {
更新A對應(yīng)的DOM節(jié)點
}
if ( B 數(shù)據(jù)變了 ) {
更新B對應(yīng)的DOM節(jié)點
}
為了更加直觀的理解,我們模擬更新一下33個數(shù)據(jù)的組件,編譯得到的p 函數(shù)打印出來,如:

我們會發(fā)現(xiàn),里面就是一大堆if判斷,但是if判斷條件比較有意思,我們從上面摘取一行仔細觀察一下:

首先要注意,&不是邏輯與,而是按位與,會把兩邊數(shù)值轉(zhuǎn)為二進制后進行比較,只有相同的二進制位都為1 才會為真。
這里的if判斷條件是:拿compoenent.$.dirty[0](00000000000000000000000000000100)和4(4 轉(zhuǎn)變?yōu)槎M制是0000 0100)做按位并操作。那么我們可以思考一下了,這個按位并操作什么時候會返回1呢?
4是一個常量,轉(zhuǎn)變?yōu)槎M制是0000 0100, 第三位是1。那么也就是,只有dirty[0]的二進制的第三位也是1時, 表達式才會返回真。換句話來說,只有第三個數(shù)據(jù)是臟數(shù)據(jù),才會走入到這個if判斷中,執(zhí)行set_data(t5, ctx[2]), 更新t5這個 DOM 節(jié)點。
當我們分析到這里,已經(jīng)看出了一些眉目,讓我們站在更高的一個層次去看待這 30多行代碼:它們其實是保存了這33個變量 和 真實DOM 節(jié)點之間的對應(yīng)關(guān)系,哪些變量臟了,Svelte 會走入不同的if體內(nèi)直接更新對應(yīng)的DOM節(jié)點,而不需要復(fù)雜 Virtual DOM DIFF 算出更新哪些DOM節(jié)點;
這 30多行代碼,是Svelte 編譯了我們寫的Svelte 組件之后的產(chǎn)物,在Svelte 編譯時,就已經(jīng)分析好了,數(shù)據(jù) 和 DOM 節(jié)點之間的對應(yīng)關(guān)系,在數(shù)據(jù)發(fā)生變化時,可以非常高效的來更新DOM節(jié)點。
Vue 曾經(jīng)也是想采取這樣的思路,但是 Vue 覺得保存每一個臟數(shù)據(jù)太消耗內(nèi)存了,于是沒有采用那么細顆粒度,而是以組件級別的中等顆粒度,只監(jiān)聽到組件的數(shù)據(jù)更新,組件內(nèi)部再通過 DIFF 算法計算出更新哪些 DOM 節(jié)點。Svelte 采用了比特位的存儲方式,解決了保存臟數(shù)據(jù)會消耗內(nèi)存的問題。
整體流程
上面就是Svelte 最核心更新DOM機制,下面我們串起來整個的流程。
下面是非常簡單的一個 Svelte 組件,點擊<button>會觸發(fā)onClick事件,從而改變name 變量。

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

第一步,Svelte 會編譯我們的代碼,下圖中左邊是我們的源碼,右邊是 Svelte 編譯生成的。Svelte 在編譯過程中發(fā)現(xiàn),『咦,這里有一行代碼 name 被重新賦值了,我要插入一條make_dirty的調(diào)用』,于是當我們改寫 name 變量的時候,就會調(diào)用make_dirty方法把 name 記為臟數(shù)據(jù)。

第二步,我們來看make_diry方法究竟做了什么事情:
把對應(yīng)數(shù)據(jù)的二進制改為1
把對應(yīng)組件記為臟組件,推入到 dirty_components 數(shù)組中
調(diào)用
schedule_update()方法把flush方法推入到一幀中的微任務(wù)階段執(zhí)行。因為這樣既可以做頻繁更新 的截流,又避免了阻塞一幀中的 layout, repaint 階段的渲染。

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

一幀大概有 16ms, 大概會經(jīng)歷 layout, repaint的階段后,就可以開始執(zhí)行微任務(wù)的回調(diào)了。
flush 方法做的事情也比較簡單,就是遍歷臟組件,依次調(diào)用update方法去更新對應(yīng)的組件。

update方法除了執(zhí)行一些生命周期的方法外,最核心的一行代碼是調(diào)用p方法,p方法我們已經(jīng)在上文中介紹過很熟悉了。

p 方法的本質(zhì)就是走入到不同的if 判斷里面,調(diào)用set_data原生的 javascript 方法更新對應(yīng)的 DOM節(jié)點。

至此,我們的頁面的DOM節(jié)點就已經(jīng)更新好了。
上面的代碼均是剔除了分支邏輯的偽代碼。
Svelte 在處理子節(jié)點列表的時候,還是有優(yōu)化的算法在的。比如說[a,b,c,d] 變成 [d, a, b, c] ,但是只是非常簡單的優(yōu)化,簡單來說,是比較節(jié)點移動距離的絕對值,絕對值最小的節(jié)點被移動。
所以,嚴格意義上來說,Svelte 并不是100%無運行時,還是會引入額外的算法邏輯,只是量很少罷了。
總結(jié)
一個前端框架,不管是vue還是react更新了數(shù)據(jù)之后,需要考慮更新哪個dom節(jié)點,也就是,需要知道,臟數(shù)據(jù)和待更新的真實dom之間的映射。vue, react 是通過 virtualDom 來 diff 計算出更新哪些dom節(jié)點更劃算,而sveltedom 是把數(shù)據(jù)和真實dom之間的映射關(guān)系,在編譯的時候就通過AST等算出來,保存在p函數(shù)中。
Svelte 作為新興的前端框架,采用了和 React, Vue 不同的設(shè)計思路,其獨特的特性在某些場景下還是很值得嘗試的。
