Svelte 原理浅析与评测

379次阅读  |  发布于3年以前

简介

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

2 . 修改相关的文件配置:

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

};

3 . 增加组件内容

如下定义了一个组件内容

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

4 . 在项目目录执行 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 name="viewport" content="width=device-width, initial-scale=1.0" />

    <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

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8