在這篇文章中,我們將會介紹 Svelte 框架的特性、優缺點和底層原理。
前端領域是發展迅速,各種輪子層出不窮的行業。最近這些年,隨著三大框架React、Vue、Angular版本逐漸穩定,前端技術棧的迭代似乎緩慢下來,React 16版本推出了 Fiber, Vue 3。0 也已經在襁褓之中。
如果我們把目光拉伸到未來十年的視角,前端行業會出現哪些框架有可能會挑戰React或者Vue呢?我們認為,嶄露頭角的 Svelte 應該是其中的選項之一。
1。Svelte 簡介
Svelte叫法是[Svelte], 本意是苗條纖瘦的,是一個新興熱門的前端框架。
在最新的《State of JS survey of 2020》中,它被預測為未來十年可能取代React和Vue等其他框架的新興技術。如果你不確定自己是否該瞭解 Svelte,可以先看一下 Svelte 的一些發展趨勢。
2。開發者滿意度
從2019年開始, Svelte出現在榜單中。剛剛過去的2020年,Svelte在滿意度排行榜中超越了react,躍升到了第一位。
3。開發者興趣度
在開發者興趣度方面,Svelte 蟬聯了第一。
4。市場佔有率
如果你在19年還沒有聽說過Svelte,不用緊張,因為svelte 當時仍是小眾的開發框架,在社群裡仍然沒有流行開來。
2020年,Svelte 的市場佔有率從第6名躍升到第4名,僅次於 React、Angular、Vue 老牌前端框架。
5。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裡面了,除了引入這個元件本身,你不需要再額外引入一個執行程式碼。
5。Svelte 優勢有哪些
我們先來看一下 Svelte 和React,Vue 相比,有哪些優勢。
5。1 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,體積上的優勢還是相當明顯的。
5。2 Less-Code ——寫更少的程式碼
在寫svelte元件時,你就會發現,和 Vue 或 React 相比只需要更少的程式碼。開發者的夢想之一,就是敲更少的程式碼。因為更少的程式碼量,往往意味著有更好的語義性,也有更少的機率寫出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設定狀態。而在Svelte中,可以直接使用賦值運算子更新狀態。
如果說上面的例子太簡單了,可以看下面的統計,分別使用 React 和 Svelte 實現下面的元件所需要的程式碼行數
下面還是 Jacek Schae 老哥的統計,編寫同樣的Realword 應用,各個框架所需要的行數
Vue 和 React 打了平手,Svelte 遙遙領先,可以少些 1000 行程式碼耶!早日下班,指日可待。
5。3 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 效能還不錯,至少沒有我們預期的那麼糟糕?我們接下來會在原理那一小結來介紹。
6。Svelte 劣勢
說完了 Svelte 的優勢,我們也要考慮到 Svelte 的劣勢。
和Vue, React框架的對比
在構建大型前端專案時,我們在選擇框架的時候就需要考慮更多的事情。Svelte 目前尚處在起步階段,對於大型專案必要的
單元測試
並沒有完整的方案。目前在大型應用中使用 Svelte , 需要謹慎評。
類目
Svelte
Vue
React
UI 元件庫
Material design ( 坦率的說,不好用 )
Element UI / AntD
AntD / Material design
狀態管理
官網自帶
Vuex
Redux/MobX
路由
Svelte-router
Vue-router
React-router
服務端渲染
支援
支援
支援
測試工具
官方網站沒有相關內容
@vue/test-utils
Jest
我們在用 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生成的程式碼的閾值了。
7。原理篇
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判斷,邏輯非常簡單
if ( A 資料變了 ) { 更新A對應的DOM節點}if ( B 資料變了 ) { 更新B對應的DOM節點}
為了更加直觀的理解,我們模擬更新一下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 元件,點選
上面程式碼背後的整體流程如下圖所示,我們一步一步來看:
第一步,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節點就已經更新好了。
1。上面的程式碼均是剔除了分支邏輯的虛擬碼
2。Svelte 在處理子節點列表的時候,還是有最佳化的演算法在的。比如說[a,b,c,d] 變成 [d, a, b, c] ,但是隻是非常簡單的最佳化,簡單來說,是比較節點移動距離的絕對值,絕對值最小的節點被移動。
3。所以,嚴格意義上來說,Svelte 並不是100%無執行時,還是會引入額外的演算法邏輯,只是量很少罷了。
總結:一個前端框架,不管是vue還是react更新了資料之後,需要考慮更新哪個dom節點,也就是,需要知道,髒資料和待更新的真實dom之間的對映。vue, react 是透過 virtualDom 來 diff 計算出更新哪些dom節點更划算,而sveltedom 是把資料和真實dom之間的對映關係,在編譯的時候就透過AST等算出來,儲存在p函式中。
Svelte 作為新興的前端框架,採用了和 React, Vue 不同的設計思路,其獨特的特性在某些場景下還是很值得嘗試的。
參考資料
原文連結:https://zhuanlan。zhihu。com/p/350507037