性能文章>Svelte runtime 源码浅析>

Svelte runtime 源码浅析原创

3月前
187013

一、什么是 Svelte


Svelte 是一种全新的构建用户界面的方法。传统框架如 React 和 Vue 需要在「浏览器」中做大量的工作,而 Svelte 将这些工作转移到构建应用程序的「编译阶段」来处理。
不同于使用虚拟 DOM 进行差异对比,Svelte 编写的代码会在应用状态变更时像做外科手术一样精准更新 DOM。

二、Svelte 的特点

1、 代码量少

我们分别用 React、Vue、Svelte 写一个相同功能的 count 变量的计数器。

React
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  
  const handleClick = () => {
    setCount(count+1)
  }
  
  return (
    <main>
      <button onClick={handleClick}>点击</button>
      <div>{count}</div>
    </main>
  )
}

export default App
Vue
<script setup>
  import { ref } from '@vue/reactivity'

  let count = ref(0)

  const handleClick = () => {
    count.value += 1
  }
</script>

<template>
  <main>
    <button @click='handleClick'>点击</button>
    <div>{{count}}</div>
  </main>
</template>
Svelte
<script>
  let count = 0
  
  function handleClick() {
    count += 1
  }
</script>

<main>
  <button on:click={handleClick}>点击</button>
  <div>{count}</div>
</main>


代码量分别为14,13,10行,显而易见 Svelte 代码量是最少的,而且基本没什么特殊语法。

2、无虚拟 DOM

Svelte 在 compiler 阶段会将代码直接编译成框架无关的纯 js 代码,执行效率更高,而 Vue 和 React 都需要经过一层虚拟 DOM。

3、真正的响应式

无需复杂的状态管理库,Svelte 使用 js 语言本身的能力实现了响应式,没有额外的心智模型,更加直观易用。

4、产物体积更小?

由于 Svelte 减少了 runtime 层,编译后的产物体积会更小,不过这个并不绝对,随着应用组件数量的增加,Svelte 的体积会更快的上升。

image1.png

5、性能更好?

Svelte 舍弃了虚拟 DOM,并且执行的是纯 js 代码,在性能测试中的表现会相对优异,不过这点同样受应用规模的影响。

image2.png

三、runtime 源码浅析

1、调试步骤

执行 npx degit sveltejs/template svelte-demo 脚本新建项目,然后安装依赖

克隆 svelte 仓库,安装依赖、运行。执行 npm link 注册软链接

在 svelte-demo 中执行 npm link svelte 执行软链接

2、修改 App.svelte 代码如下

<script>
  let count = 0

  function handleClick() {
    count += 1
  }
</script>

<main>
  <button on:click={handleClick}>点击</button>
  <div>{count}</div>
</main>

将代码放在 svelte 在线代码编译模板(https://www.sveltejs.cn/examples#hello-world)中,查看编译结果如下

image3.png

3、分析编译后的代码

代码为函数 create_fragment、函数 instance、App 类三部分。最下面的 App 类为最终导出的结果,可以看到主要是将 instance 和 create_fragment 函数传入构造函数中,然后调用 init 函数,构造 App 对象。

class App extends SvelteComponent {
  constructor(options) {
    super()
    init(this, options, instance, create_fragment, safe_not_equal{})
  }
}

export default App

接下来看 instance 源码。

function instance($$self, $$props, $$invalidate) {
  let count = 0

  function handleClick() {
    $$invalidate(0, count += 1)
  }

  return [count, handleClick]
}

instance 函数返回了一个数组,元素为我们定义的 count 和 handleClick,代码比较简单,其中在 handleClick 函数中调用了 instance 的参数 $$invalidate。

$$invalidate 为一个函数,第一个参数是当前变量在 instance 模板的位置,第二个参数为更新的方法。

接下来看 create_fragment 函数。

function create_fragment(ctx) {
  let main;
  let button;
  let t1;
  let div;
  let t2;
  let mounted;
  let dispose;

  return {
    c() {
      main = element("main");
      button = element("button");
      button.textContent = "点击";
      t1 = space();
      div = element("div");
      t2 = text(/*count*/ ctx[0]);
      attr(main, "class", "svelte-1tky8bj");
    },

    m(target, anchor) {
      insert(target, main, anchor);
      append(main, button);
      append(main, t1);
      append(main, div);
      append(div, t2);

      if (!mounted) {
        dispose = listen(button, "click", /*handleClick*/ ctx[1]);
        mounted = true;
      }
    },

    p(ctx, [dirty]) {
      if (dirty & /*count*/ 1) set_data(t2, /*count*/ ctx[0]);
    },

    i: noop,

    o: noop,

    d(detaching) {
      if (detaching) detach(main);
      mounted = false;
      dispose();
    }
  };
}

可以看到函数主要是和 DOM 操作相关的,我们接下来简单分析一下。

  • create_fragment 入参为 ctx,即上面 instance 函数执行后返回的数组(里面元素为声明的变量和函数)。

  • create_fragment 返回一个对象,对象中的不同函数对应页面加载的不同阶段。

  • c 为 create 函数,分别将 svelte 模板的每一段节点块创建一个变量标签(创建真实 DOM)。

  • m 为 mounted 函数,将每一个变量标签添加到父 DOM 节点中,并且调用 listen 方法给 button 添加 DOM 的 click 事件,函数为 ctx[1] 即 handleClick(插入真实 DOM)。

  • p 为 update 函数,调用 set_data,将 ctx[0] 即 count 的变量(真实 DOM 的赋值)DOM 中更新最新的变量值。

  • d 为 destory 函数,调用 detach 函数销毁 DOM 并且将 mounted 赋值为 false,并且调用 dispose 函数移除事件的监听。

4、 分析 runtime 中的代码

由上面可以看到 compile 中的 App 类最终会调用 init 方法。

在源码中 svelte/src/runtime/internal/Component.ts 中第109行,把处理边缘 case 的代码精简后如下。

export function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) {
  const $$: T$$ = component.$$ = {
    fragment: null,
    ctx: null,
    // state
    props,
    update: noop,
    not_equal,
    bound: blank_object(),
    // lifecycle
    on_mount: [],
    on_destroy: [],
    on_disconnect: [],
    before_update: [],
    after_update: [],
    context: new Map(options.context || (parent_component ? 
parent_component.$$.context : [])),
    // everything else
    callbacks: blank_object(),
    dirty,
    skip_bound: false,
    root: options.target || parent_component.$$.root
  };

  // 调用传入instance的函数将返回的变量和方法数组赋值给$$.ctx变量,同时在instance函数中接收
  // 组件更新后的方法,将更新后的新值传给make_dirty函数进行更新
  $$.ctx = 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);
      if (ready) make_dirty(component, i);
    }

    return ret;
  }) : [];

  // 调用before_update函数
  run_all($$.before_update);

  // 调用create_fragment函数将返回的真实Dom赋值给$$.fragment,入参为$$.ctx变量
  $$.fragment = create_fragment ? create_fragment($$.ctx) : false;

  // 调用组件的create方法
  $$.fragment && $$.fragment!.c();

  //调用组件的mounted方法
  mount_component(component, options.target, options.anchor, options.customElement);

  // 刷新视图,更新页面
  flush();
}


5、Svelte 整个流程简图

image4.jpeg

  1. Svelte 在编译时将组件中的变量和方法封装在 instance 函数中,返回值为一个数组。将组件中的 DOM 和生命周期封装在 create_fragment 函数中。然后调用运行时的 init 方法将 instance 和 create_fragment 传入。
  2. 当组件点击时将调用 instance 函数返回值中对应的事件,将组件中变量的下标和最新值返回并且调用 init 中的回调函数。
  3. 在 init 的回调函数中调用 make_dirty 标记需要更新的值。
  4. 然后进行视图刷新后调用 create_fragment 函数中的 update 函数将对应的值更新到 DOM 中。

总结

Svelte 在编译阶段将大部分准备工作做好,在运行时候只需要进行脏检查和视图刷新,调用编译好的组件的更新函数即可。这种设计思路在当前前端环境下还是非常有新意的,在开发者中的评价也比较高,大家有兴趣可以尝试一下。

最后,简单总结一下:

「优点」:上手简单,使用起来心智负担低;代码简洁;开发效率高;性能好。

「缺点」:生态不完善,组件库少;组件过多时体积会膨胀;大型复杂应用的实践较少。

我们是数数科技前端团队,目前负责游戏行业使用最多的用户行为分析系统的前端研发,同时也在积极探索前端新技术和新领域,如果你对游戏、大数据、可视化、工程化、全栈等方面有兴趣,欢迎加入我们,共创未来!

点赞收藏
thinkingdata

数数科技前端团队,专注于大数据、可视化和工程化。

请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

从linux源码看socket的阻塞和非阻塞

从linux源码看socket的阻塞和非阻塞

从linux源码看socket的close

从linux源码看socket的close

源码分析 RocketMQ DLedger(多副本) 之日志复制

源码分析 RocketMQ DLedger(多副本) 之日志复制

Redis 源码简洁剖析8—Reactor 模型

Redis 源码简洁剖析8—Reactor 模型

Redis源码简洁剖析13—Redis 持久化

Redis源码简洁剖析13—Redis 持久化

硬核剖析ThreadLocal源码,面试官看了直呼内行

硬核剖析ThreadLocal源码,面试官看了直呼内行

3
1