Svelte 原理淺析與評測

簡介


Svelte 是一個構建 web 應用程序的工具,與 React 和 Vue 等 JavaScript 框架類似,都懷揣着一顆讓構建交互式用戶界面變得更容易的心。

但是有一個關鍵的區別:Svelte 在 構建 / 編譯階段 將你的應用程序轉換爲理想的 JavaScript 應用,而不是在 運行階段 解釋應用程序的代碼。這意味着你不需要爲框架所消耗的性能付出成本,並且在應用程序首次加載時沒有額外損失。

你可以使用 Svelte 構建整個應用程序,也可以逐步將其融合到現有的代碼中。你還可以將組件作爲獨立的包(package)交付到任何地方,並且不會有傳統框架所帶來的額外開銷。

特點

代碼簡潔

我們以一個簡單例子來說明,在輸入框中輸入內容,然後在彈窗中顯示相關內容。然後將 svelte 的代碼與 react、vue 作一下對比,可以很明顯的發現,svelte 要寫的代碼量遠少於 react 和 vue。

<script>

  let animal = 'dog';

  const showModal = () ={

    alert(`My favorite animal is ${animal}`);

  };

</script>



<input type="text" bind:value={animal} />

<button on:click={showModal}>彈出</button>
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>

    </>

  );

}
<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:

在 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 文件的內容進行解析。

最終 parse 會將.svelte 的內容解析成含有 htmlcssinstancemodule 四部分的 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 引入了 appenddetachelementinsertlisten 等方法,從源碼 [7] 中可以知道都是一些很簡單的對原生 dom 操作的封裝。

create_fragment

create_fragment 是和每個組件生成 dom 相關的方法,裏面定義了 cmpiod 等一系列內置方法,從縮寫上不好理解,我們可以看下源碼 [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;

}

instance

instance 方法中返回了包含組件實例中屬性和方法的數組,將相應的數據綁定在組件實例的 $$.ctx 上,並且根據用戶定義的觸發屬性修改的方法去調用一個 $$invalidate方法,我們來看下$$invalidate這個方法幹了什麼:

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。

髒檢測

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 中會異步去執行 flush 函數:

export function schedule_update() {

  if (!update_scheduled) {

    update_scheduled = true;

    resolved_promise.then(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 生命週期鉤子函數,將 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);

  }

}

回到上面的 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 官方目前沒有自己的路由,社區實現的路由庫:

SSR

目前官方主推的 ssr 框架,具備以下的特點:

sapper 開發比較早,也是官方的 ssr 框架,但是 Rich Harris 在 2020 年 10 月的 svelte 峯會上表示:sapper 永遠不會發布 1.0 版本。也就是說 sapper 不會發布穩定版甚至被放棄,而 svelte kit 則是它的繼任者。

跨平臺

Svelte 偏向於性能,目前在跨平臺方面還沒有進行探究。

svelte-native[14] (社區庫)

暫不支持

可以 electron[15] 結合開發桌面應用

組件庫

Svelte 現在組件庫數量尚可,但是都不夠完備,如 table 等複雜組件都沒有實現

測試工具

缺少官方的測試工具,社區單元測試庫:

svelte-testing-library[19]

總結來說,svelte 的周邊生態目前還不夠完備,但由於起步較晚可以理解。

VSCode 插件

Typescript

支持

CSS 預處理器

支持 less、 scss 及 postcss

使用 svelte 構建 web component

我們平臺組最近正在進行 web component 組件庫開發的選型調研,svelte 也作爲備選的框架之一。傳統的框架如 vue、react 如果想要開發 web component,需要每個組件都打包一份體積龐大的運行時,而 svelte 的運行時會根據你的功能按需引入,所以十分適合 web component 的開發場景。

配置

下面是通過 svelte 開發一個簡單的 web component 的實例:

  1. 通過官方提供的腳手架創建一個組件
npx degit sveltejs/component-template custom-test-button
  1. 修改相關的文件配置:

修改 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()],

};
  1. 增加組件內容

如下定義了一個組件內容

<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>
  1. 在項目目錄執行 npm run build 將組件打包,假設打包後的文件爲 index.js

使用

<!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 的 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>

同 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