新興前端框架 Svelte 從入門到原理
在這篇文章中,我們將會介紹 Svelte 框架的特性、優缺點和底層原理。
本文儘量不會涉及 Svelte 的語法,大家可以放心食用。因爲 Svelte 的語法極其簡單,而且官方教程學習曲線平緩 https://www.sveltejs.cn/,相信大家很快就會上手語法的,這裏就不做官網搬運工了。
前端領域是發展迅速,各種輪子層出不窮的行業。最近這些年,隨着三大框架React
、Vue
、Angular
版本逐漸穩定,前端技術棧的迭代似乎緩慢下來,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
都有58k
,React
更有97.5k
。我們使用 React 開發一個小型組件,即使裏面的邏輯代碼很少,但是打包出來的 bundle size 輕輕鬆鬆都要 100k 起步。對於大型後臺管理系統來說,100k 不算什麼,但是對於特別注重用戶端加載性能的場景來說,一個組件 100k 多,還是太大了。
如果你特別在意打包出來的體積,Svelte 就是一個特別好的選擇。下面是Jacek Schae
大神的統計,使用市面上主流的框架,來編寫同樣的 Realword 應用的體積:
從上圖的統計,Svelte 簡直是神奇!竟然只有 9.7 KB ! 果然魔法消失 UI 框架,無愧其名。
可以看出,Svelte
的bundle size
大小是Vue
的 1/4,是React
的 1/20,體積上的優勢還是相當明顯的。
Less-Code ——寫更少的代碼
在寫svelte
組件時,你就會發現,和 Vue 或 React 相比只需要更少的代碼。開發者的夢想之一,就是敲更少的代碼。因爲更少的代碼量,往往意味着有更好的語義性,也有更少的幾率寫出 bug。
下面的例子,可以看出Svelte
和React
的不同:
React
的代碼
1const [count, setCount] = useState(0);
2
3function increment() {
4 setCount(count + 1);
5}
6
7
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 開發公司級別中大型項目時,也發現了其他的一些主要注意的點
- 沒有像 AntD 那樣成熟的 UI 庫。比如說需求方想加一個 toast 提示,或者彈窗,pm:” 很簡單的,不用出 UI 稿,就直接用之前的樣式好啦~“
但是 Svelte 需要從 0 開始 ” 抄 “ 出來一個 toast 或者彈窗組件出來,可能會帶來額外的開發量和做好加班的準備。
-
Svelte 原生不支持預處理器,比如說
less
/scss
,需要自己單獨的配置 webpack loader。 -
Svelte 原生腳手架沒有目錄劃分
-
暫時不支持 typescript,雖然官方說了會支持, 但是不知道什麼時候.
還需要注意的一點是,React / Vue 等框架自帶的runtime
雖然會增加首屏加載的bundle.js
,可是當項目變得越來越大的時候,框架的runtime
在bundle.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
,shouldComponentUpdate
,useMemo
,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,則代表着該數據是否是髒數據。如果是髒數據,則意味着更新。
-
第一行
["0000000000000000000000000000001", "0000000000000000000000000000000"]
, 表示第一個數據髒了,需要更新第一個數據對應的 dom 節點 -
第二行
["0000000000000000000000000000011", "0000000000000000000000000000000"]
, 表示第一個、第二個數據都髒了,需要更新第一個,第二個數據對應的 dom 節點。 -
……
當一個組件內,數據的個數,超出了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
-
把對應組件記爲髒組件,推入到 dirty_components 數組中
-
調用
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 節點更划算,而svelte
dom 是把數據和真實 dom 之間的映射關係,在編譯的時候就通過 AST 等算出來,保存在p
函數中。
Svelte 作爲新興的前端框架,採用了和 React, Vue 不同的設計思路,其獨特的特性在某些場景下還是很值得嘗試的。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Lf7diVKMmqqLiglUON4CnA