Svelte 原理淺析與評測
簡介
Svelte 是一個構建 web 應用程序的工具,與 React 和 Vue 等 JavaScript 框架類似,都懷揣着一顆讓構建交互式用戶界面變得更容易的心。
但是有一個關鍵的區別:Svelte 在 構建 / 編譯階段 將你的應用程序轉換爲理想的 JavaScript 應用,而不是在 運行階段 解釋應用程序的代碼。這意味着你不需要爲框架所消耗的性能付出成本,並且在應用程序首次加載時沒有額外損失。
你可以使用 Svelte 構建整個應用程序,也可以逐步將其融合到現有的代碼中。你還可以將組件作爲獨立的包(package)交付到任何地方,並且不會有傳統框架所帶來的額外開銷。
特點
代碼簡潔
我們以一個簡單例子來說明,在輸入框中輸入內容,然後在彈窗中顯示相關內容。然後將 svelte 的代碼與 react、vue 作一下對比,可以很明顯的發現,svelte 要寫的代碼量遠少於 react 和 vue。
- 使用 svelte,代碼如下:
<script>
let animal = 'dog';
const showModal = () => {
alert(`My favorite animal is ${animal}`);
};
</script>
<input type="text" bind:value={animal} />
<button on:click={showModal}>彈出</button>
- react:
import React, { useState } from 'react';
export default function App() {
const [animal, setAnimal] = useState('dog');
const showModal = () => {
alert(`My favorite animal is ${animal}`);
};
return (
<>
<input
type="text"
value={animal}
onChange={() => {
setAnimal(animal);
}}
/>
<button onClick={showModal}>彈出</button>
</>
);
}
- Vue
<template>
<div>
<input type="text" v-model="animal" />
<button @click="showModal">彈出</button>
</div>
</template>
<script>
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const animal = ref('dog');
const showModal = () => {
alert(`My favorite animal is ${animal.value}`);
};
return {
animal,
showModal,
};
},
});
</script>
無虛擬 dom
Svelte 能夠將代碼編譯成體積小、不依賴框架的普通 js 代碼,讓應用程序無論是啓動還是運行都很迅速。
性能更好
許多同學在學習 react 或者 vue 時可能聽說過諸如 “虛擬 dom 很快” 之類的言論,所以看到這裏就會疑惑,svelte 沒有虛擬 dom,爲什麼反而更快呢?
這其實是一個誤區,react 和 vue 等框架實現虛擬 dom 的最主要的目的不是性能,而是爲了掩蓋底層 dom 操作,讓用戶通過聲明式的、基於狀態驅動 UI 的方式去構建我們的應用程序,提高代碼的可維護性。
另外 react 或者 vue 所說的虛擬 dom 的性能好,是指我們在沒有對頁面做特殊優化的情況下,框架依然能夠提供不錯的性能保障。例如以下場景,我們每次從服務端接收數據後就重新渲染列表,如果我們通過普通 dom 操作不做特殊優化,每次都重新渲染所有列表項,性能消耗比較高。而像 react 等框架會通過 key 對列表項做標記,只對發生變化的列表項重新渲染,如此一來性能便提高了。
思考上面這個場景,如果我們操作真實 dom 時也對列表項做標記,只對發生變化的列表項重新渲染,省去了虛擬 dom diff 等環節,那麼性能是比虛擬 dom 還要高的。
svelte 便實現了這種優化,通過將數據和真實 dom 的映射關係,在編譯的時候通過 ast 計算並保存起來,數據發生變動時直接更新 dom,由於不依賴虛擬 dom,初始化和更新時都都十分迅速。
體積更小?
我們都知道 react 和 vue 都是基於運行時的框架,打包後除了用戶自己編寫的代碼之外,還有框架本身的 runtime。而 svelte 是通過靜態編譯減少框架運行時的代碼量。
https://www.npmtrends.com/react-vs-react-dom-vs-vue-vs-svelte[1]
參照 npm trends,react、vue 和 svelte 的 minzipped 體積分別爲:42.2kb、22.9kb 和 1.6kb,足以看出 svelte 的短小精悍。
但是上面這個單看框架的體積稍微有些片面,svelte 由於在編譯時將組件直接解釋爲 js,所以相對來說組件編譯後的代碼量會比 vue 和 react 編譯後要大一些。假如有 n 個組件,svelte 每個組件編譯後個規模爲 a,vue 或者 react 每個組件編譯後的規模爲 b:
-
svelte:體積 = a * n
-
vue:體積 = S + b * n (S 爲框架運行時的體積)
在 a > b 的情況下,隨着 n 的數量的增多,svelte 項目在體積上並不會佔據太大的優勢。
與 vue 對比
Vue 方面,尤雨溪曾將 vue3 和 svelte 做了對比:https://github.com/yyx990803/vue-svelte-size-analysis[2]
基於真實的 todomvc 場景構建組件,編譯以後 Svelte 的組件輸出大小是 Vue 的 1.7 倍,在 SSR 的情況下,這一比例會上升到 2.1 倍。在不開啓 SSR 的情況下,大概 19 個組件後就會抹平運行時體積的大小差異,開啓 SSR 的情況下,大概 13 個組件後就會抹平差異。
與 react 對比
Jacek Schae 也曾將 svelte 和 react 進行對比,也是在組件數量達到一定的閾值之後, svelte 的體積優勢就不再存在。
可見,大型項目中使用 svelte 的體積問題還有待考究。
真正的 reactivity
無需複雜的狀態管理庫,Svelte 爲 JavaScript 自身添加反應能力。後面的源碼解讀部分會講解 svelte 的響應式實現。
發展趨勢
Svelte 是 Rich Harris[3] (rollup 作者),2016 年 svelte 開始開源, 2019 年開始引起較爲廣泛的關注。
Github 上 svelte[4] 現在是 49.9k star:
Npm 上 svelte[5] 的周下載量大概在 15w 左右:
雖然從 star 數和下載量來說離 react、vue 和 angular 還有較大差距,但是鑑於其出道比較晚也是可以理解。而且從框架的調研 [6] 來看,近兩年來其用戶滿意度和感興趣度都是高居第一,使用和知名度也是在急速上升的。
總體來看,未來可期!
源碼解讀
svelte 的源碼由兩大部分組成,compiler 和 runtime。compiler 的作用是將 svelte 模版語法編譯爲瀏覽器能夠識別的 js SvelteComponent,而 runtime 則是在瀏覽器中幫助業務代碼運作的運行時函數。
complier
Svelte 如其介紹所說,在 complier 階段完成了大部分的工作,而 complier 又分爲 parse 和 complie 兩部分:
parse
parse 會讀取 .svelte
文件的內容進行解析。
-
對於 html 部分的內容,會分爲 tag、mustache、text 三種解析類型:
-
tag: tag 解析的內容以
<
作爲標識,包括 HTMLElement、style 標籤、script 標籤以及用戶自定義的 svelte 組件以及 svelte 實現的一些特殊標籤如svelte:head
、svelte:options
、svelte:window
以及svelte:body
等。 -
muscache:mustache 以
{
作爲標識,識別的內容除了模板語法之外,還包括 svelte 的邏輯渲染 (else……if、each) 等語法、{``@html``}
、{``@debug}
等。 -
text 就是無語義的靜態文本
最終 parse 會將.svelte
的內容解析成含有 html
、css
、instance
、module
四部分的 ast。
Instance 是指 script 標籤中響應式的屬性和方法,module 是使用 <script context="module"
聲明 的無響應的變量和方法。
complie
Complie 首先會將 parse 過程中拿到的語法樹(包含 html,css,instance 和 module)轉換爲 Component,然後在 render_dom 中通過 code-red 中的 print 函數將 component 的轉換爲 js 可運行代碼,最終輸出 complier 的結果。
runtime
我們以一個簡單的例子來看下,點擊按鈕,count 加 1:
<script>
let count = 0;
const addCount = () => {
count += 1;
};
</script>
<div>
<button on:click={addCount}>增加</button>
<p>count is: {count}</p>
</div>
svelte 編譯後的結果爲:
/* App.svelte generated by Svelte v3.42.4 */
import {
SvelteComponent,
append,
detach,
element,
init,
insert,
listen,
noop,
safe_not_equal,
set_data,
space,
text,
} from 'svelte/internal';
function create_fragment(ctx) {
let div;
let button;
let t1;
let p;
let t2;
let t3;
let mounted;
let dispose;
return {
c() {
div = element('div');
button = element('button');
button.textContent = '增加';
t1 = space();
p = element('p');
t2 = text('count is: ');
t3 = text(/*count*/ ctx[0]);
},
m(target, anchor) {
insert(target, div, anchor);
append(div, button);
append(div, t1);
append(div, p);
append(p, t2);
append(p, t3);
if (!mounted) {
dispose = listen(button, 'click', /*addCount*/ ctx[1]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t3, /*count*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(div);
mounted = false;
dispose();
},
};
}
function instance($$self, $$props, $$invalidate) {
let count = 0;
const addCount = () => {
$$invalidate(0, (count += 1));
};
return [count, addCount];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default App;
我們看到編譯後的結果中,有一個 create_fragement
的方法和 instance
方法。
另外還從 svelte/internal
引入了 append
、detach
、element
、insert
、listen
等方法,從源碼 [7] 中可以知道都是一些很簡單的對原生 dom 操作的封裝。
create_fragment
create_fragment
是和每個組件生成 dom 相關的方法,裏面定義了 c
、 m
、p
、i
、o
、d
等一系列內置方法,從縮寫上不好理解,我們可以看下源碼 [8] 中其類型定義:
export interface Fragment {
key: string|null;
first: null;
/* create */ c: () => void;
/* claim */ l: (nodes: any) => void;
/* hydrate */ h: () => void;
/* mount */ m: (target: HTMLElement, anchor: any) => void;
/* update */ p: (ctx: any, dirty: any) => void;
/* measure */ r: () => void;
/* fix */ f: () => void;
/* animate */ a: () => void;
/* intro */ i: (local: any) => void;
/* outro */ o: (local: any) => void;
/* destroy */ d: (detaching: 0|1) => void;
}
-
c(create):create 函數里會對一系列的 dom 節點進行創建,並將它們保存在 create_fragment 的閉包中。
-
m(mount):mount 函數中會對根據 dom 節點的層級關係,構建 dom 樹,並將 dom 樹插入到頁面 target 節點下。同時還會將 dom 上相關的事件監聽進行綁定,然後將組件的 mounted 狀態置爲 true。
-
p(update):組件的狀態發生改變時會觸發 update 函數,對 dom 中相應的數據重新進行更新渲染。
-
d(distroy):將 dom 掛載從頁面中移除,將組件的 mounted 狀態置爲 false,同時移除一系列的事件監聽。
instance
instance
方法中返回了包含組件實例中屬性和方法的數組,將相應的數據綁定在組件實例的 $$.ctx
上,並且根據用戶定義的觸發屬性修改的方法去調用一個 $$invalidate
方法,我們來看下$$invalidate
這個方法幹了什麼:
$$.instance
:
instance(component, options.props || {}, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = value))) {
if (!$$.skip_bound && $$.bound[i]) $.bound[i](value "i] "i]) $.bound[i") $.bound[i");
if (ready) make_dirty(component, i);
}
return ret;
});
$$invalidate
接收 2 個或更多參數。第一個參數是 i 是 屬性在 $$.ctx
,第二個參數 ret 是定義的屬性改變的邏輯函數。然後判斷屬性重新賦值後與之前的值是否相等,若不相等,則會調用 make_dirty
更新相關的 ui。
髒檢測
- make_dirty:
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
}
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}
每個組件的 $$
屬性上有一個 dirty 數組,用於標記 instance 中需要更新的屬性下標,當 dirty 第一項爲 -1
時,表示這個組件當前是乾淨的,將其 push 到 dirty_components 中,然後執行 schedule_update
方法。
- schedule_update:
schedule_update 中會異步去執行 flush 函數:
export function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
- flush:
flush 中對剛剛的 dirty_components 進行遍歷,執行 update 函數.
for (let i = 0; i < dirty_components.length; i += 1) {
const component = dirty_components[i];
set_current_component(component);
update(component.$$);
}
- Update
update 函數會調用組件 update
生命週期鉤子函數,將 dirty 數組重新置爲 -1,然後調用 fragment 的 p(update) 去更新 ui。
function update($$) {
if ($$.fragment !== null) {
$$.update();
run_all($$.before_update);
const dirty = $$.dirty;
$$.dirty = [-1];
$$.fragment && $$.fragment.p($$.ctx, dirty);
$$.after_update.forEach(add_render_callback);
}
}
- dirty 標記
回到上面的 make_dirty
方法,svelte 是通過如下操作對屬性進行髒標記的:
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
瞭解了位運算,那我們看上面髒標記的代碼,(i / 31) | 0
對每個 instance 返回的數組下標除以 31 後和 0 做或運算,即除 31 向下取整,(1 << (i % 31))
i 對 31 取餘之後向左進行移位操作。通過上述的兩步操作,可以瞭解到 dirty 數組存儲了一系列的 32 位整數,通過這一操作,提高了內存利用率,每個數組項可以存儲 31 個屬性是否需要更新。
例如如下 32 位的整數 43,對應的 32 位二進制爲:
Dirty = [43]
43 -> 0000 0000 0000 0000 0000 0000 0010 1011
二進制中爲 1 的位代表需要更新的 instance 中數組第幾項,即第 1、2、4、6 項屬性需要更新。
整體流程
周邊生態
狀態管理
Svelte 框架中自己實現了 store[9],無需安裝單獨的狀態管理庫。
路由
Svelte 官方目前沒有自己的路由,社區實現的路由庫:
-
svelte-routing[10],1.4k star,npm 周下載量 3.5k,和 react-router-dom 的使用方式很像
-
svelte-spa-router[11],886 star,npm 周下載量 3.5k,更類似於 vue-router 的使用方式
SSR
- sveltekit[12]
目前官方主推的 ssr 框架,具備以下的特點:
-
服務端渲染(SSR)
-
路由
-
typescript 支持
-
less, scss 支持
-
serverless
-
vite 打包
-
Sapper[13]
sapper 開發比較早,也是官方的 ssr 框架,但是 Rich Harris 在 2020 年 10 月的 svelte 峯會上表示:sapper 永遠不會發布 1.0 版本。也就是說 sapper 不會發布穩定版甚至被放棄,而 svelte kit 則是它的繼任者。
跨平臺
Svelte 偏向於性能,目前在跨平臺方面還沒有進行探究。
- native
svelte-native[14] (社區庫)
- 小程序
暫不支持
- 桌面應用
可以 electron[15] 結合開發桌面應用
組件庫
Svelte 現在組件庫數量尚可,但是都不夠完備,如 table 等複雜組件都沒有實現
-
svelte-material-ui[16] 1.8k star,扁平化設計,偏向 google 的 UI 設計系統。
-
carbon-components-svelte[17] 1k star,偏商務,非主流,偏向 IBM 的開源設計系統。
-
smelte[18] 1k star,material 的另一種方案
測試工具
缺少官方的測試工具,社區單元測試庫:
svelte-testing-library[19]
總結來說,svelte 的周邊生態目前還不夠完備,但由於起步較晚可以理解。
VSCode 插件
-
Svelte for VS Code[20] 識別 svelte 語法,對 svelte 進行語法高亮
-
Svelte 3 Snippets[21] 在 vscode 中通過簡寫快速生成 svelte 相關語法
-
Svelte Intellisense[22] svelte 組件 command + click 快速跳轉及 svelte 引入省略
.svelte
文件後綴
Typescript
支持
CSS 預處理器
支持 less、 scss 及 postcss
使用 svelte 構建 web component
我們平臺組最近正在進行 web component 組件庫開發的選型調研,svelte 也作爲備選的框架之一。傳統的框架如 vue、react 如果想要開發 web component,需要每個組件都打包一份體積龐大的運行時,而 svelte 的運行時會根據你的功能按需引入,所以十分適合 web component 的開發場景。
配置
下面是通過 svelte 開發一個簡單的 web component 的實例:
- 通過官方提供的腳手架創建一個組件
npx degit sveltejs/component-template custom-test-button
- 修改相關的文件配置:
修改 package.json
包名稱
{
"name": "CustomTestButton",
"svelte": "src/index.js",
"module": "dist/index.mjs",
"main": "dist/index.js",
"scripts": {
"build": "rollup -c",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^9.0.0",
"rollup": "^2.0.0",
"rollup-plugin-svelte": "^6.0.0",
"svelte": "^3.0.0"
},
"keywords": [
"svelte"
],
"files": [
"src",
"dist"
]
}
修改 rollup.config.js
文件的內容:
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import pkg from './package.json';
const name = pkg.name
.replace(/^(@\S+/)?(svelte-)?(\S+)/, '$3')
.replace(/^\w/, (m) => m.toUpperCase())
.replace(/-\w/g, (m) => m[1].toUpperCase());
export default {
input: 'src/index.js',
output: [
{ file: pkg.module, format: 'es' },
{ file: pkg.main, format: 'umd', name },
],
plugins: [svelte({ customElement: true }), resolve()],
};
- 增加組件內容
如下定義了一個組件內容
<svelte:options tag="custom-test-button" />
<script>
export let value = '點擊';
export let type = 'default';
</script>
<button class={`custom-test-button ${type}`}>{value}</button>
<style>
.custom-test-button {
height: 32px;
padding: 0 8px;
box-sizing: border-box;
line-height: 32px;
font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.2);
background-color: #fff;
}
.primary {
background-color: #42b983;
color: #fff;
border: none;
}
.danger {
background-color: #f44336;
color: #fff;
border: none;
}
</style>
- 在項目目錄執行
npm run build
將組件打包,假設打包後的文件爲index.js
使用
- Html 引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta />
<title>svelte web component</title>
</head>
<body>
<script src="./index.js"></script>
<custom-test-button value="測試按鈕" type="danger"></custom-test-button>
</body>
</html>
- vue 引入
在 vue 的 html 中引入 index.js
:
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<script src="./custom.js"></script>
<!-- built files will be auto injected -->
</body>
然後在 vue 組件中使用
<template>
<div>
<custom-test-button :value="'測試'" type="danger"></custom-test-button>
</div>
</template>
- React 中引入
同 vue,就不做過多介紹了
總結
整體來說,svelte 繼前端三大框架之後推陳出新,以一種新的思路實現了響應式,由於起步時間不算很長,目前來說其生態還不夠完備, 在大型項目中的應用目前也還有待考究,但是在一些簡單頁面如活動頁、靜態頁等場景感覺目前還是十分適合的,個人對其未來發展表示看好。
由於其簡潔的語法以及與 vue 語法相似的特點,上手成本十分小,大家感興趣可以稍花一點點時間瞭解一下,豐富自己的武器庫。
參考資料
[1]
https://www.npmtrends.com/react-vs-react-dom-vs-vue-vs-svelte: https://www.npmtrends.com/react-vs-react-dom-vs-vue-vs-svelte
[2]
https://github.com/yyx990803/vue-svelte-size-analysis: https://github.com/yyx990803/vue-svelte-size-analysis
[3]
Rich Harris: https://github.com/Rich-Harris
[4]
svelte: https://github.com/sveltejs/svelte
[5]
svelte: https://www.npmjs.com/package/svelte
[6]
調研: https://2020.stateofjs.com/en-US/technologies/front-end-frameworks/
[7]
源碼: https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dom.ts
[8]
源碼: https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/Component.ts
[9]
store: https://github.com/sveltejs/svelte/blob/master/src/runtime/store/index.ts
[10]
svelte-routing: https://github.com/EmilTholin/svelte-routing
[11]
svelte-spa-router: https://github.com/italypaleale/svelte-spa-router
[12]
sveltekit: https://github.com/sveltejs/kit
[13]
Sapper: https://github.com/sveltejs/sapper
[14]
svelte-native: https://github.com/halfnelson/svelte-native
[15]
electron: https://github.com/electron/electron
[16]
svelte-material-ui: https://github.com/hperrin/svelte-material-ui
[17]
carbon-components-svelte: https://github.com/carbon-design-system/carbon-components-svelte
[18]
smelte: https://smeltejs.com/
[19]
svelte-testing-library: https://github.com/testing-library/svelte-testing-library
[20]
Svelte for VS Code: https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode
[21]
Svelte 3 Snippets: https://marketplace.visualstudio.com/items?itemName=fivethree.vscode-svelte-snippets
[22]
Svelte Intellisense: https://marketplace.visualstudio.com/items?itemName=ardenivanov.svelte-intellisense
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/TiC4FYAS5px5PjUk6hoD4A