Svelte 原理淺析與評測
大廠技術(shù)??堅持周更??精選好文
簡介
Svelte 是一個構(gòu)建 web 應(yīng)用程序的工具,與 React 和 Vue 等 JavaScript 框架類似,都懷揣著一顆讓構(gòu)建交互式用戶界面變得更容易的心。
但是有一個關(guān)鍵的區(qū)別:Svelte 在 構(gòu)建/編譯階段 將你的應(yīng)用程序轉(zhuǎn)換為理想的 JavaScript 應(yīng)用,而不是在 運(yùn)行階段 解釋應(yīng)用程序的代碼。這意味著你不需要為框架所消耗的性能付出成本,并且在應(yīng)用程序首次加載時沒有額外損失。
你可以使用 Svelte 構(gòu)建整個應(yīng)用程序,也可以逐步將其融合到現(xiàn)有的代碼中。你還可以將組件作為獨(dú)立的包(package)交付到任何地方,并且不會有傳統(tǒng)框架所帶來的額外開銷。
特點
代碼簡潔
我們以一個簡單例子來說明,在輸入框中輸入內(nèi)容,然后在彈窗中顯示相關(guān)內(nèi)容。然后將svelte的代碼與react、vue作一下對比,可以很明顯的發(fā)現(xiàn),svelte要寫的代碼量遠(yuǎn)少于react和vue。
使用svelte,代碼如下:
type="text"?bind:value={animal}?/>
react:
import?React,?{?useState?}?from?'react';
export?default?function?App()?{
??const?[animal,?setAnimal]?=?useState('dog');
??const?showModal?=?()?=>?{
????alert(`My?favorite?animal?is?${animal}`);
??};
??return?(
????<>
??????
????????type="text"
????????value={animal}
????????onChange={()?=>?{
??????????setAnimal(animal);
????????}}
??????/>
??????
????>
??);
}
Vue
??
????type="text"?v-model="animal"?/>
????
??
無虛擬dom
Svelte 能夠?qū)⒋a編譯成體積小、不依賴框架的普通js代碼,讓應(yīng)用程序無論是啟動還是運(yùn)行都很迅速。
性能更好
許多同學(xué)在學(xué)習(xí) react 或者 vue 時可能聽說過諸如“虛擬dom很快”之類的言論,所以看到這里就會疑惑,svelte 沒有虛擬dom,為什么反而更快呢?
這其實是一個誤區(qū),react 和 vue 等框架實現(xiàn)虛擬 dom 的最主要的目的不是性能,而是為了掩蓋底層 dom 操作,讓用戶通過聲明式的、基于狀態(tài)驅(qū)動UI的方式去構(gòu)建我們的應(yīng)用程序,提高代碼的可維護(hù)性。
另外 react 或者 vue 所說的虛擬 dom 的性能好,是指我們在沒有對頁面做特殊優(yōu)化的情況下,框架依然能夠提供不錯的性能保障。例如以下場景,我們每次從服務(wù)端接收數(shù)據(jù)后就重新渲染列表,如果我們通過普通dom操作不做特殊優(yōu)化,每次都重新渲染所有列表項,性能消耗比較高。而像react等框架會通過key對列表項做標(biāo)記,只對發(fā)生變化的列表項重新渲染,如此一來性能便提高了。

思考上面這個場景,如果我們操作真實dom時也對列表項做標(biāo)記,只對發(fā)生變化的列表項重新渲染,省去了虛擬dom diff等環(huán)節(jié),那么性能是比虛擬dom還要高的。
svelte便實現(xiàn)了這種優(yōu)化,通過將數(shù)據(jù)和真實dom的映射關(guān)系,在編譯的時候通過 ast 計算并保存起來,數(shù)據(jù)發(fā)生變動時直接更新dom,由于不依賴虛擬dom,初始化和更新時都都十分迅速。
體積更小?
我們都知道 react 和 vue 都是基于運(yùn)行時的框架,打包后除了用戶自己編寫的代碼之外,還有框架本身的 runtime。而 svelte 是通過靜態(tài)編譯減少框架運(yùn)行時的代碼量。
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 每個組件編譯后個規(guī)模為 a,vue 或者 react 每個組件編譯后的規(guī)模為 b:
svelte:體積 = a * n vue:體積 = S + b * n (S為框架運(yùn)行時的體積)
在 a > b 的情況下,隨著 n 的數(shù)量的增多,svelte 項目在體積上并不會占據(jù)太大的優(yōu)勢。
與 vue 對比
Vue 方面,尤雨溪曾將 vue3 和 svelte 做了對比:https://github.com/yyx990803/vue-svelte-size-analysis[2]

基于真實的 todomvc 場景構(gòu)建組件,編譯以后Svelte 的組件輸出大小是Vue的1.7倍,在 SSR 的情況下,這一比例會上升到2.1倍。在不開啟 SSR 的情況下,大概19個組件后就會抹平運(yùn)行時體積的大小差異,開啟 SSR 的情況下,大概 13 個組件后就會抹平差異。
與 react 對比
Jacek Schae 也曾將 svelte 和 react進(jìn)行對比,也是在組件數(shù)量達(dá)到一定的閾值之后, svelte 的體積優(yōu)勢就不再存在。

可見,大型項目中使用 svelte 的體積問題還有待考究。
真正的reactivity
無需復(fù)雜的狀態(tài)管理庫,Svelte 為 JavaScript 自身添加反應(yīng)能力。后面的源碼解讀部分會講解 svelte 的響應(yīng)式實現(xiàn)。
發(fā)展趨勢
Svelte 是 Rich Harris[3] (rollup 作者),2016 年 svelte 開始開源, 2019 年開始引起較為廣泛的關(guān)注。
Github 上 svelte[4] 現(xiàn)在是 49.9k star:

Npm 上 svelte[5] 的周下載量大概在 15w 左右:

雖然從 star 數(shù)和下載量來說離 react、vue 和 angular 還有較大差距,但是鑒于其出道比較晚也是可以理解。而且從框架的調(diào)研[6]來看,近兩年來其用戶滿意度和感興趣度都是高居第一,使用和知名度也是在急速上升的。

總體來看,未來可期!
源碼解讀
svelte 的源碼由兩大部分組成,compiler 和 runtime。compiler 的作用是將 svelte 模版語法編譯為瀏覽器能夠識別的js SvelteComponent,而 runtime 則是在瀏覽器中幫助業(yè)務(wù)代碼運(yùn)作的運(yùn)行時函數(shù)。
complier
Svelte 如其介紹所說,在 complier 階段完成了大部分的工作,而 complier 又分為 parse 和 complie 兩部分:
parse
parse 會讀取 .svelte 文件的內(nèi)容進(jìn)行解析。
對于 html 部分的內(nèi)容,會分為 tag、mustache、text 三種解析類型:
tag: tag 解析的內(nèi)容以 <作為標(biāo)識,包括 HTMLElement、style標(biāo)簽、script標(biāo)簽以及用戶自定義的 svelte 組件以及 svelte 實現(xiàn)的一些特殊標(biāo)簽如svelte:head、svelte:options、svelte:window以及svelte:body等。muscache:mustache 以 {作為標(biāo)識,識別的內(nèi)容除了模板語法之外,還包括 svelte 的邏輯渲染(else……if、each)等語法、{``@html``}、{``@debug}等。text就是無語義的靜態(tài)文本
最終parse會將.svelte 的內(nèi)容解析成含有 html 、css 、instance 、module 四部分的ast。
Instance 是指 script 標(biāo)簽中響應(yīng)式的屬性和方法,module 是使用 count?is:?{count}
??
??
svelte編譯后的結(jié)果為:
/*?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;
我們看到編譯后的結(jié)果中,有一個 create_fragement 的方法和 instance 方法。
另外還從 svelte/internal 引入了 append、detach 、element 、insert 、listen 等方法,從源碼[7]中可以知道都是一些很簡單的對原生 dom 操作的封裝。
create_fragment
create_fragment 是和每個組件生成 dom 相關(guān)的方法,里面定義了 c 、 m 、p 、i 、o 、d 等一系列內(nèi)置方法,從縮寫上不好理解,我們可以看下源碼[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 函數(shù)里會對一系列的 dom 節(jié)點進(jìn)行創(chuàng)建,并將它們保存在 create_fragment 的閉包中。 m(mount):mount 函數(shù)中會對根據(jù) dom 節(jié)點的層級關(guān)系,構(gòu)建 dom 樹,并將 dom 樹插入到頁面 target 節(jié)點下。同時還會將 dom 上相關(guān)的事件監(jiān)聽進(jìn)行綁定,然后將組件的 mounted 狀態(tài)置為 true。 p(update):組件的狀態(tài)發(fā)生改變時會觸發(fā) update 函數(shù),對 dom 中相應(yīng)的數(shù)據(jù)重新進(jìn)行更新渲染。 d(distroy):將 dom 掛載從頁面中移除,將組件的 mounted 狀態(tài)置為 false,同時移除一系列的事件監(jiān)聽。
instance
instance 方法中返回了包含組件實例中屬性和方法的數(shù)組,將相應(yīng)的數(shù)據(jù)綁定在組件實例的 $$.ctx 上,并且根據(jù)用戶定義的觸發(fā)屬性修改的方法去調(diào)用一個 $$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個或更多參數(shù)。第一個參數(shù)是 i 是 屬性在 $$.ctx,第二個參數(shù) ret 是定義的屬性改變的邏輯函數(shù)。然后判斷屬性重新賦值后與之前的值是否相等,若不相等,則會調(diào)用 make_dirty 更新相關(guān)的 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 數(shù)組,用于標(biāo)記 instance 中需要更新的屬性下標(biāo),當(dāng) dirty 第一項為 -1 時,表示這個組件當(dāng)前是干凈的,將其 push 到 dirty_components 中,然后執(zhí)行 schedule_update 方法。
schedule_update:
schedule_update 中會異步去執(zhí)行 flush 函數(shù):
export?function?schedule_update()?{
??if?(!update_scheduled)?{
????update_scheduled?=?true;
????resolved_promise.then(flush);
??}
}
flush:
flush 中對剛剛的 dirty_components 進(jìn)行遍歷,執(zhí)行 update 函數(shù).
for?(let?i?=?0;?i?
??const?component?=?dirty_components[i];
??set_current_component(component);
??update(component.$$);
}
Update
update函數(shù)會調(diào)用組件 update 生命周期鉤子函數(shù),將 dirty 數(shù)組重新置為 -1,然后調(diào)用 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 標(biāo)記
回到上面的 make_dirty 方法,svelte 是通過如下操作對屬性進(jìn)行臟標(biāo)記的:
component.$$.dirty[(i?/?31)?|?0]?|=?(1?<(i?%?31));

了解了位運(yùn)算,那我們看上面臟標(biāo)記的代碼,(i / 31) | 0 對每個 instance 返回的數(shù)組下標(biāo)除以 31 后和 0 做或運(yùn)算,即除 31 向下取整,(1 << (i % 31)) i 對 31 取余之后向左進(jìn)行移位操作。通過上述的兩步操作,可以了解到 dirty 數(shù)組存儲了一系列的 32 位整數(shù),通過這一操作,提高了內(nèi)存利用率,每個數(shù)組項可以存儲31個屬性是否需要更新。
例如如下32位的整數(shù)43,對應(yīng)的32位二進(jìn)制為:
Dirty = [43]
43 -> 0000 0000 0000 0000 0000 0000 0010 1011
二進(jìn)制中為1的位代表需要更新的 instance 中數(shù)組第幾項,即第1、2、4、6項屬性需要更新。
整體流程

周邊生態(tài)
狀態(tài)管理
Svelte 框架中自己實現(xiàn)了 store[9],無需安裝單獨(dú)的狀態(tài)管理庫。
路由
Svelte 官方目前沒有自己的路由,社區(qū)實現(xiàn)的路由庫:
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 框架,具備以下的特點:
服務(wù)端渲染(SSR)
路由
typescript支持
less, scss支持
serverless
vite打包
Sapper[13]
sapper開發(fā)比較早,也是官方的 ssr 框架,但是 Rich Harris 在2020年10月的svelte峰會上表示:sapper永遠(yuǎn)不會發(fā)布1.0版本。也就是說 sapper 不會發(fā)布穩(wěn)定版甚至被放棄,而 svelte kit 則是它的繼任者。
跨平臺
Svelte 偏向于性能,目前在跨平臺方面還沒有進(jìn)行探究。
native
svelte-native[14] (社區(qū)庫)
小程序
暫不支持
桌面應(yīng)用
可以 electron[15] 結(jié)合開發(fā)桌面應(yīng)用
組件庫
Svelte 現(xiàn)在組件庫數(shù)量尚可,但是都不夠完備,如 table 等復(fù)雜組件都沒有實現(xiàn)
svelte-material-ui[16] 1.8k star,扁平化設(shè)計,偏向 google 的 UI 設(shè)計系統(tǒng)。 carbon-components-svelte[17] 1k star,偏商務(wù),非主流,偏向 IBM 的開源設(shè)計系統(tǒng)。 smelte[18] 1k star,material 的另一種方案
測試工具
缺少官方的測試工具,社區(qū)單元測試庫:
svelte-testing-library[19]
總結(jié)來說,svelte 的周邊生態(tài)目前還不夠完備,但由于起步較晚可以理解。
VSCode插件
Svelte for VS Code[20] 識別 svelte 語法,對 svelte 進(jìn)行語法高亮 Svelte 3 Snippets[21] 在 vscode 中通過簡寫快速生成 svelte 相關(guān)語法 Svelte Intellisense[22] svelte 組件 command + click 快速跳轉(zhuǎn)及 svelte 引入省略 .svelte文件后綴
Typescript
支持
CSS 預(yù)處理器
支持 less、 scss 及 postcss
使用 svelte 構(gòu)建 web component
我們平臺組最近正在進(jìn)行 web component 組件庫開發(fā)的選型調(diào)研,svelte 也作為備選的框架之一。傳統(tǒng)的框架如 vue、react 如果想要開發(fā)web component,需要每個組件都打包一份體積龐大的運(yùn)行時,而 svelte 的運(yùn)行時會根據(jù)你的功能按需引入,所以十分適合 web component 的開發(fā)場景。
配置
下面是通過 svelte 開發(fā)一個簡單的 web component 的實例:
通過官方提供的腳手架創(chuàng)建一個組件
npx?degit?sveltejs/component-template?custom-test-button
修改相關(guān)的文件配置:
修改 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 文件的內(nèi)容:
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()],
};
增加組件內(nèi)容
如下定義了一個組件內(nèi)容
"custom-test-button"?/>
在項目目錄執(zhí)行 npm run build將組件打包,假設(shè)打包后的文件為index.js
使用
Html 引入
"en">
??
????"UTF-8"?/>
????"X-UA-Compatible"?content="IE=edge"?/>
????"viewport"?content="width=device-width,?initial-scale=1.0"?/>
????svelte?web?component
??
??
??????
????"測試按鈕"?type="danger">
??
vue 引入
在 vue 的 html 中引入 index.js:
????
????"app">
????
????
??
然后在 vue 組件中使用
??
????"'測試'"?type="danger">
??
React 中引入
同 vue,就不做過多介紹了
總結(jié)
整體來說,svelte 繼前端三大框架之后推陳出新,以一種新的思路實現(xiàn)了響應(yīng)式,由于起步時間不算很長,目前來說其生態(tài)還不夠完備, 在大型項目中的應(yīng)用目前也還有待考究,但是在一些簡單頁面如活動頁、靜態(tài)頁等場景感覺目前還是十分適合的,個人對其未來發(fā)展表示看好。
由于其簡潔的語法以及與 vue 語法相似的特點,上手成本十分小,大家感興趣可以稍花一點點時間了解一下,豐富自己的武器庫。
參考資料
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]調(diào)研: 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
???謝謝支持
以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^
喜歡的話別忘了?分享、點贊、收藏?三連哦~。
歡迎關(guān)注公眾號?ELab團(tuán)隊?收貨大廠一手好文章~
我們來自字節(jié)跳動,是旗下大力教育前端部門,負(fù)責(zé)字節(jié)跳動教育全線產(chǎn)品前端開發(fā)工作。
我們圍繞產(chǎn)品品質(zhì)提升、開發(fā)效率、創(chuàng)意與前沿技術(shù)等方向沉淀與傳播專業(yè)知識及案例,為業(yè)界貢獻(xiàn)經(jīng)驗價值。包括但不限于性能監(jiān)控、組件庫、多端技術(shù)、Serverless、可視化搭建、音視頻、人工智能、產(chǎn)品設(shè)計與營銷等內(nèi)容。
字節(jié)跳動校/社招內(nèi)推碼:?CBP6BY9
投遞鏈接:?https://job.toutiao.com/s/8RWRqsn
