最近有一个官网页,打算用svelte体验一下,顺便学习了一下svelte(发音:[svelt]),整体来说,svelte是比较简洁的,上手很快。不过与其说是一个前端框架,不如说是一个“dom操作编译器”。svelte的开发代码,在编译阶段会被编译成一系列的dom操作的代码,运行时的代码很少。因此svelte.js的体积很小(只保留了脏值检测更新和封装dom操作API等core代码)。本文从一下几个方面聊一聊对于svelte的认识。
- svelte初体验
 - svelte的语法
 - Virtual Dom和Dom
 - 优缺点
 - svelte源码阅读
 
原文地址在我的博客: github.com/fortheallli…
一、svelte初体验
我们直接来看官网的例子:

实现的功能也很简单,就是两个Input的值求和,然后展示出来。用svelte编写的代码为:
<script>
        let a = 1;
        let b = 2;
</script>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {a + b}</p>
上述代码很简洁,像vue一样也是类似style dom script的三段式写法,不过比vue更加简洁一点,比如dom不需要template包裹等等。
同样的上述的例子的代码如果用react书写:
import React, { useState } from 'react';
export default () => { 
    const [a, setA] = useState(1); 
    const [b, setB] = useState(2); 
    function handleChangeA(event) { setA(+event.target.value); } 
    function handleChangeB(event) { setB(+event.target.value); }
    return ( 
       <div> 
          <input type="number" value={a} onChange={handleChangeA}/> 
          <input type="number" value={b} onChange={handleChangeB}/> 
          <p>{a} + {b} = {a + b}</p> 
       </div> 
    );
}
上述react的写法,必须要先弄懂useState的含义等,此外缺少了默认的双向数据绑定,代码有一点冗余。
同样的上述的例子的代码如果用vue书写:
<template> 
    <div> 
       <input type="number" v-model.number="a"> 
       <input type="number" v-model.number="b"> 
       <p>{{a}} + {{b}} = {{a + b}}</p> 
   </div> 
</template> 
<script> 
    export default { 
       data: function() { 
          return { a: 1, b: 2 }; 
       } 
    }; 
</script>
三者对比:
| 框架名称 | svelte | react | vue | demo字符数 | 145 | 445 | 263 | 
|---|
单纯的说,svelte编码只需要145个字符,比vue和react少,因此得出说svelte的编码体积更小,这样是不对的,因为svelte会在编译阶段将代码编译到更加贴近dom操作的代码,上述例子的代码,编译后的结果为:
/* App.svelte generated by Svelte v3.38.3 */
    import {
            SvelteComponent,
            append,
            attr,
            detach,
            element,
            init,
            insert,
            listen,
            noop,
            run_all,
            safe_not_equal,
            set_data,
            set_input_value,
            space,
            text,
            to_number
    } from "svelte/internal";
    function create_fragment(ctx) {
            let input0;
            let t0;
            let input1;
            let t1;
            let p;
            let t2;
            let t3;
            let t4;
            let t5;
            let t6_value = /*a*/ ctx[0] + /*b*/ ctx[1] + "";
            let t6;
            let mounted;
            let dispose;
            return {
                    c() {
                            input0 = element("input");
                            t0 = space();
                            input1 = element("input");
                            t1 = space();
                            p = element("p");
                            t2 = text(/*a*/ ctx[0]);
                            t3 = text(" + ");
                            t4 = text(/*b*/ ctx[1]);
                            t5 = text(" = ");
                            t6 = text(t6_value);
                            attr(input0, "type", "number");
                            attr(input1, "type", "number");
                    },
                    m(target, anchor) {
                            insert(target, input0, anchor);
                            set_input_value(input0, /*a*/ ctx[0]);
                            insert(target, t0, anchor);
                            insert(target, input1, anchor);
                            set_input_value(input1, /*b*/ ctx[1]);
                            insert(target, t1, anchor);
                            insert(target, p, anchor);
                            append(p, t2);
                            append(p, t3);
                            append(p, t4);
                            append(p, t5);
                            append(p, t6);
                            if (!mounted) {
                                    dispose = [
                                            listen(input0, "input", /*input0_input_handler*/ ctx[2]),
                                            listen(input1, "input", /*input1_input_handler*/ ctx[3])
                                    ];
                                    mounted = true;
                            }
                    },
                    p(ctx, [dirty]) {
                            if (dirty & /*a*/ 1 && to_number(input0.value) !== /*a*/ ctx[0]) {
                                    set_input_value(input0, /*a*/ ctx[0]);
                            }
                            if (dirty & /*b*/ 2 && to_number(input1.value) !== /*b*/ ctx[1]) {
                                    set_input_value(input1, /*b*/ ctx[1]);
                            }
    
                            if (dirty & /*a*/ 1) set_data(t2, /*a*/ ctx[0]);
                            if (dirty & /*b*/ 2) set_data(t4, /*b*/ ctx[1]);
                            if (dirty & /*a, b*/ 3 && t6_value !== (t6_value = /*a*/ ctx[0] + /*b*/ ctx[1] + "")) set_data(t6, t6_value);
                    },
                    i: noop,
                    o: noop,
                    d(detaching) {
                            if (detaching) detach(input0);
                            if (detaching) detach(t0);
                            if (detaching) detach(input1);
                            if (detaching) detach(t1);
                            if (detaching) detach(p);
                            mounted = false;
                            run_all(dispose);
                    }
            };
    }
    
    function instance($$self, $$props, $$invalidate) {
            let a = 1;
            let b = 2;
            function input0_input_handler() {
                    a = to_number(this.value);
                    $$invalidate(0, a);
            }
            function input1_input_handler() {
                    b = to_number(this.value);
                    $$invalidate(1, b);
            }
            return [a, b, input0_input_handler, input1_input_handler];
    }
    
    class App extends SvelteComponent {
            constructor(options) {
                    super();
                    init(this, options, instance, create_fragment, safe_not_equal, {});
            }
    }
    
    export default App;
在编译后生成的代码其实代码量也不小,是远远大于145个字符的,也不能说因为编译后的代码量大,所以说svelte有点名不副实,并不能减少运行时代码的体积。要考虑到svelte的运行时代码是很少的.我们来对比一下:
| 框架名称 | react | vue | angular | svelte | 体积 | 42k | 22k | 89.5k | 1.6k | 
|---|
从上述对比中可以看出,svelte的体积很少,虽然其业务代码在编译后会生产较多的代码。得益于较少的运行时代码。虽然svelte代码的随着业务的编写增量速度比较快,得益于其很小的包体积1.6k,对于一般中小型项目而言,整体运行的代码(编译后的代码+包体积)还是比较小的,所以可以说svelte项目的代码较小。不过对于大型项目而言,因为svelte随着业务的进行,运行时代码增量陡峭,大型项目体积并不会比react、vue等小,因此需要辩证看待。
此外虽说svelte的代码在编译后体积很大,但是在编译前的代码,其实很简洁,这种简洁,一定程度上,可以增强开发体验。
二、 svelte的语法
svelte的写法跟vue有点类似,是指令式和响应式的。
- 
基本用法
 
<script>
    let name = 'world';
</script>
<h1>Hello {name}!</h1>
<style>
  h1{
    color:red
  }
</style>
这是一个最简单的hello world的例子,上述代码中很简洁。在编译后的代码分为js编译和css编译。
- js编译
 
/* App.svelte generated by Svelte v3.38.3 */
import {
        SvelteComponent,
        attr,
        detach,
        element,
        init,
        insert,
        noop,
        safe_not_equal
} from "svelte/internal";
function create_fragment(ctx) {
        let h1;
        return {
                c() {
                        h1 = element("h1");
                        h1.textContent = `Hello ${name}!`;
                        attr(h1, "class", "svelte-khrn1o");
                },
                m(target, anchor) {
                        insert(target, h1, anchor);
                },
                p: noop,
                i: noop,
                o: noop,
                d(detaching) {
                        if (detaching) detach(h1);
                }
        };
}
let name = "world";
class App extends SvelteComponent {
        constructor(options) {
                super();
                init(this, options, null, create_fragment, safe_not_equal, {});
        }
}
export default App;
svelte/internal包中是一些封装了dom操作的函数。
- 
css编译结果:
h1.svelte-khrn1o{color:red} 
css是通过创建style标签引入到最后的dom中的。
- 
指令形式和数据绑定
 
<script>
    let a = 1;
    let b = 2;
    $: total =  a+b
</script>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {total}</p>
还是以上面的例子为例,上述就是一个指令形式+数据绑定的形式。跟vue的写法很相似,改例子绑定了input和a, input和b.效果如下:

这里的$total: 就是reactive statement. 类似vue中的计算属性。
- 
组件compose
 
//Name.svelte
<script lang='typescript'>
    export let name = "yuxl"
</script>
<span>
    {name}
</span>
//Age.svelte
<script lang='typescript'>
    export let age = 18
</script>
<span>
    {age}
</span>
//index.svelte
<script>
import Name from './Name.svelte'
import Age from './Age.svelte' 
</script>
<div>
   <Name name="some name"/>
   <Age age = {20} />
</div>
在svelte中的组件的compose也是跟react中类似的,不同的是在react中export的属性就是组件的props,写法上比较简洁,此外,export const 和export function、export class这3个组件的props是只读的,不可写。
- 
模版语法
 
在svelte中,html相关的场景适用于模版语法,最简单的模版语法为:
{#if answer === 42} <p>what was the question?</p> {/if}
这里介绍几个在svelte中几个比较有趣的模版语法。
- @debug
 
<script>  
    export let name = "yuxl"
</script>
 {@debug name}
  <span>
    {name}
  </span>
运行debugger的结果为:
@debug 在后面跟的参数name发生变化的时候会进行debugger,从上图我们看到debugger的地方上下文的代码是编译后运行时,跟编码的时候有一点区别,也进一步说明,svelte可以看作是一个前端的编译框架,真正运行时的代码是编译后的结果。
- @html
 
<script lang='typescript'>
    export let name = "yuxl"
    const age = '<span>20</span>'
</script>
<div>
    <span>{name}</span>
    {@html age}
</div>
- 
#await 用法为:{#await expression}...{:then name}...{:catch name}...{/await}
<script lang='typescript'> const promise = new Promise((resolve)=>{ setTimeout(()=>{ resolve("success") },2000) }) </script> <div> {#await promise} <!-- promise is pending --> <p>waiting for the promise to resolve...</p> {:then value} <!-- promise was fulfilled --> <p>The value is {value}</p> {/await} </div> 
- 
动画效果
 
在svelte中,对于原始的dom元素,自带了一些动画指令,在一般的官网或者活动页中,场景最多的就是动画效果,svelte自带的动画指令,因此在写官网的时候方便了不少。
以transition:fly为例:
<script>
    import { fly } from 'svelte/transition';
    let visible = true;
</script>
<label>
   <input type="checkbox" bind:checked={visible}>
    visible
</label>
{#if visible}
    <p transition:fly="{{ y: 200, duration: 2000 }}">
            Flies in and out
    </p>
{/if}
最后的结果为:

当然在svelte中也支持自定义动画指令。
- 
组件的生命周期
 
    svelte组件也提供了完整的生命周期。onMount、beforeUpdate、afterUpdate、onDestroy等。见名思意,这里不一一介绍,跟react & vue的组件生命周期近似。
除了上述之外,svelte还支持自定义元素(custom element), store以及context等等。
三、Virtual Dom和Dom
这个其实可以,比较客观的去看待,svelte的作者认为,Virtual Dom的性能并没有太大的问题,不管是diff算法还是render的过程都没有什么性能问题,不过作者认为,svelte不需要diff,还是有一点优势的。虽然diff很快,但是没有diff的话,显然会更快的得到渲染结果。
svelte的编译后的结果来看,所有的dom的变动都变为了直接的dom操作行为,是不需要做diff的,这种方法,没有diff/patch,因此从速度来看,肯定更快一些。 比如:
<script>
import { fade } from "svelte/transition";
let visible = false
function handleClick(){
    visible = true
}
</script>
<div>
    <div on:click={handleClick}>点击</div>
    {#if visible}
        <div transition:fade ="{{ duration: 2000 }}" >
            fades in and out
        </div>
    {/if}
</div>
上述这个例子中,修改了visible,编译后的代码知道这个行为,这是一个确定的会如何影响dom的行为,编译后的结果部分为:

可以看到,state的改变如何影响dom在svelte的编译结果中都是很确定的。
除了性能问题,svelte的作者认为,因为virtualDom的存在,需要保存new object和old object的虚拟dom对象,在react的编程中,每一次渲染都有这两个对象,这两个对象,在正常的开发中,很容易添加一些冗余代码:
function MoreRealisticComponent(props) {
          const [selected, setSelected] = useState(null);
          return (
            <div>
              <p>Selected {selected ? selected.name : 'nothing'}</p>
        
              <ul>
                {props.items.map(item =>
                  <li>
                    <button onClick={() => setSelected(item)}>
                      {item.name}
                    </button>
                  </li>
                )}
              </ul>
            </div>
          );
    }
在这个例子中,为每一个li都绑定了一个事件,这是不过度优化情况下的正常下发,因为virtualDom虚拟dom的存在,每一次state更新的时候,每一个new object和old object都包含了每一个li的绑定函数,这些是冗余的代码,增加了代码的体积等。
四、优缺点
个人归纳了一下几个优缺点:
- 
优点:
- 体积很小,是真的小,包体积只有1.6k,对于小型项目比如官网首页,活动页等确实可以拿来试试。上手也很快,很轻量级。类似活动页这种简单页面的lowcoder系统,也可以尝试一下,因为框架本身提供的api,应该是目前前端框架里面最简单的。
 - no virtual dom的形式一定程度上确实要快一些,没有了diff/path
 
 - 
缺点
- 虽然包的体积小,但是编译后的代码其实并不小,代码总量的增加曲线其实还是有一定陡峭的。在大型项目中没有证明自己。
 - 生态问题,生态其实并不是很完善,虽然类似的比如组件库之类的都有,但是没有很完善。
 
五、源码阅读
 
首先svelte的源码分为两部分,compiler和runtime,compiler主要的作用是将开发代码编译成运行时的代码,具体如何编译不是本文所要关注的代码。本文主要关注的是编译后的运行时的代码runtime。
- 
dom操作相关core api
 
我们以最简单的hello world为例: svelte编译前源码:
<h1>Hello world!</h1>
svelte编译后的代码:
import {
        SvelteComponent,
        detach,
        element,
        init,
        insert,
        noop,
        safe_not_equal
} from "svelte/internal";
function create_fragment(ctx) {
        let h1;
        return {
                c() {
                        h1 = element("h1");
                        h1.textContent = "Hello world!";
                },
                m(target, anchor) {
                        insert(target, h1, anchor);
                },
                p: noop,
                i: noop,
                o: noop,
                d(detaching) {
                        if (detaching) detach(h1);
                }
        };
}
class App extends SvelteComponent {
        constructor(options) {
                super();
                init(this, options, null, create_fragment, safe_not_equal, {});
        }
}
export default App;
这里的App就可以直接使用了,比如渲染到一个父dom中可以这样使用:
import App from './App.svelte'
var app = new App({
  target: document.body,
});
export default app;
上述方法就可以把App这个编译后的运行时组件渲染到body中,我们来看编译后的代码。
- create_fragment
 
在svelte组件中,与dom相关的部分封装在了create_fragment中,该函数创建了一个Fragment, 该函数返回一个包含dom操作的对象:
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对应创建一个子dom元素,m表示创建元素要渲染元素时需要执行的函数,d表示删除元素时的操作。上述的例子中:
function create_fragment(ctx) {
        let h1;
        return {
                c() {
                        h1 = element("h1");
                        h1.textContent = "Hello world!";
                },
                m(target, anchor) {
                        insert(target, h1, anchor);
                },
                p: noop,
                i: noop,
                o: noop,
                d(detaching) {
                        if (detaching) detach(h1);
                }
        };
}
在m中的intert和d中的detach方法,都是原生的dom操作方法,上述Fragment的意思是创建了h1这个dom,并在渲染的时候插入到目标dom节点中,在Fragment这个组件元素被销毁的时候,销毁被创建的子dom元素 h1。
element、insert、detach等方法都是原生的dom操作,具体源码如下所示:
export function element<K extends keyof HTMLElementTagNameMap>(name: K) {
        return document.createElement<K>(name);
}
export function insert(target: NodeEx, node: NodeEx, anchor?: NodeEx) {
        target.insertBefore(node, anchor || null);
}
export function detach(node: Node) {
        node.parentNode.removeChild(node);
}
- SvelteComponent
 
export class SvelteComponent {
        $$: T$$;
        $$set?: ($$props: any) => void;
        $destroy() {
                destroy_component(this, 1);
                this.$destroy = noop;
        }
        $on(type, callback) {
                const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
                callbacks.push(callback);
                return () => {
                        const index = callbacks.indexOf(callback);
                        if (index !== -1) callbacks.splice(index, 1);
                };
        }
        $set($$props) {
                if (this.$$set && !is_empty($$props)) {
                        this.$$.skip_bound = true;
                        this.$$set($$props);
                        this.$$.skip_bound = false;
                }
        }
}
SvelteComponent组件定义了如何销毁组件以及如何设置组件的属性,以及如何增加监听函数,其中最重要的是定义了组件的实例属性 .
interface T$$ {
        dirty: number[];
        ctx: null|any;
        bound: any;
        update: () => void;
        callbacks: any;
        after_update: any[];
        props: Record<string, 0 | string>;
        fragment: null|false|Fragment;
        not_equal: any;
        before_update: any[];
        context: Map<any, any>;
        on_mount: any[];
        on_destroy: any[];
        skip_bound: boolean;
        on_disconnect: any[];
}
发现SvelteComponent组件确实包含了ctx上下文内容,以及组件的生命周期属性,以及组件的脏值检测等相关的属性。
- 
init函数 `js export function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
const parent_component = current_component; set_current_component(component); 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(parent_component ? parent_component.$$.context : options.context || []), // everything else callbacks: blank_object(), dirty, skip_bound: false }; let ready = false; $$.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; }) : []; $$.update(); ready = true; run_all($$.before_update); // `false` as a special case of no DOM component $$.fragment = create_fragment ? create_fragment($$.ctx) : false; if (options.target) { flush(); } set_current_component(parent_component); }init函数在SvelteComponent组件内部调用,用于实例属性的初始化。这里最重要的是$$.ctx的赋值部分,后续会用来做脏值检测。ctx中保存了所有的再多次渲染中都存在的值,包含了内部的state以及监听处理函数等等。
- 
脏值检测和更新部分
 
 - 
 
这里我们以一个带有鼠标时间的svelte组件为例,
编译前的代码:
<script>
        let m = { x: 0, y: 0 };
        function handleMousemove(event) {
                m.x = event.clientX;
                m.y = event.clientY;
        }
</script>
<div on:mousemove={handleMousemove}>
        The mouse position is {m.x} x {m.y}
</div>
svelte编译后的代码与hello world相比增加的代码:
function create_fragment(ctx) {
        let div;
        let t0;
        let t1_value = /*m*/ ctx[0].x + "";
        let t1;
        let t2;
        let t3_value = /*m*/ ctx[0].y + "";
        let t3;
        return {
               ...
                p(ctx, [dirty]) {
                        if (dirty & /*m*/ 1 && t1_value !== (t1_value = /*m*/ ctx[0].x + "")) set_data(t1, t1_value);
                        if (dirty & /*m*/ 1 && t3_value !== (t3_value = /*m*/ ctx[0].y + "")) set_data(t3, t3_value);
                },
               ...
        };
}
function instance($$self, $$props, $$invalidate) {
        let m = { x: 0, y: 0 };
        function handleMousemove(event) {
                $$invalidate(0, m.x = event.clientX, m);
                $$invalidate(0, m.y = event.clientY, m);
        }
        return [m, handleMousemove];
}
这里多了一个instance函数,而这个instance函数在svelteComponent的init函数中就是用作脏值检测和更新的。
$$.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;
    })
    : [];
如果值发生了变动,就触发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));
}
make_dirty标记了哪一些脏组件,然后对脏组件执行schedule_update方法来更新组件:
export function schedule_update() {
        if (!update_scheduled) {
                update_scheduled = true;
                resolved_promise.then(flush);
        }
}
schedule_update在需要更新时候,在下一个微任务重执行flush:
export function flush() {
        if (flushing) return;
        flushing = true;
        do {
                // first, call beforeUpdate functions
                // and update components
                for (let i = 0; i < dirty_components.length; i += 1) {
                        const component = dirty_components[i];
                        set_current_component(component);
                        update(component.$$);
                }
                set_current_component(null);
                ...
                render_callbacks.length = 0;
        } while (dirty_components.length);
}
简化后的flush方法如上所示,就是遍历整个脏组件,执行所有的脏组件中的更新方法update.update方法的定义为:
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);
        }
}
update方法标记自身组件为脏,并且制定自身组件fragment中的p(全名:update)也就是前面的fragment中的:
p(ctx, [dirty]) {
                        if (dirty & /*m*/ 1 && t1_value !== (t1_value = /*m*/ ctx[0].x + "")) set_data(t1, t1_value);
                        if (dirty & /*m*/ 1 && t3_value !== (t3_value = /*m*/ ctx[0].y + "")) set_data(t3, t3_value);
                },
在p方法中,直接操作dom改变UI。
总结来看,组件更新的步骤为以下几步:
- 事件或者其他操作出发更新流程
 - 在instance的$$invalidate方法中,比较操作前后ctx中的值有没有发生改变,如果发生改变则继续往下
 - 执行make_dirty函数标记为脏值,添加带有脏值需要更新的组件,从而继续触发更新
 - 执行schedule_update函数
 - 执行flush函数,将所有的脏值组件取出,以此执行其update方法
 - 在update方法中,执行的是Fragment自身的p方法,p方法做的事情就是确定需要更新组件,并操作和更新dom组件,从而完成了最后的流程
 
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
 - 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
 
- 提示下载完但解压或打开不了?
 
- 找不到素材资源介绍文章里的示例图片?
 
- 模板不会安装或需要功能定制以及二次开发?
 
                    
    
发表评论
还没有评论,快来抢沙发吧!