Svelte笔记三:runtime源码解读

svelte的源码很简单是由两大部分组成,compiler和runtime。

compiler就是一个编译器将svelte模版语法转换为浏览器能够识别的代码。而runtime则是在浏览器中帮助业务代码运作的运行时函数。所以说runtime是svelte框架最核心的部分,它也解释了svelte是如何在没有virtual dom的情况下也照样运行的。今天我们review一下runtime代码。

svelte的runtime主要由fragment和component组成,而component是包含了fragment。它们有着独立的生命周期,将逻辑层和渲染层分离。

Fragment

Svelte官方example提供了compile出来的Js output。这些output就是运行在浏览器的源码,根据内容知道svelte的基本运作,让开发者很清除它内部的每一步运作。下面这个栗子很简单,就是对hello的一个字符串插值。而name是一个变量。

<script>
    let name = 'world';
</script>

<h1>Hello {name}!</h1>

编译出来的结果:

/* App.svelte generated by Svelte v3.24.0 */
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 ${name}!`;
        },
        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;

编译出来的结果就是有一个初始化函数,叫create_fragment,它是用于dom的初始挂载。它使用了element函数,通过查阅源码src/runtime/internal/dom,我们知道它的作用就是用来创建h1标签实例,并且填入可变内容。除了element之外,还有spacetextsvg_element等都是用于生成真实dom,分别是对空格,纯文本,svg进行生成处理。

export function element<K extends keyof HTMLElementTagNameMap>(name: K) {
    return document.createElement<K>(name);
}

export function text(data: string) {
    return document.createTextNode(data);
}

export function space() {
    return text(' ');
}

create_fragment的过程还包含有c,m,p,i,o,d等特殊名称的函数,这些函数并非编译混淆,而是Fragment内部的生命周期缩写。Fragment指得是真实dom的节点,它拥有着独立的生命周期和属性。源码中src/runtime/internal/Component介绍了它的定义,它是一个真实的dom元素集合,它的属性并非组件属性(如下方ts类型定义),分别包含了create,claim,hydrate,mount,update,mesure,fix,animate,intro,outro,destory,组件的真实变化会影响Fragment的变化,Fragment的变化影响真实的dom,从上面例子看在create的过程中它创建了h1标签,在mount的过程将刚才创建的h1挂载到页面中,在update的过程没有任何操作,在detach的过程销毁该Fragment。

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

Component

SvelteComponent则是包含了svelte组件内置的属性和生命周期,它们与Fragment的属性和生命周期是息息相关,SvelteComponent是依赖于Fragment,组件的变化会触发Fragment的变化。它是一个相辅相成的组合。源码中还有SvelteComponent和SvelteElement的细分,不同点在于Web Component的组件的支持,这里就不再展开。

Component拥有四个生命周期,分别是mount,beforeUpdate, afterUpdate,destory。没有create阶段是因为svelte没有virtual dom。所以在组件层面,它没有像vue那么复杂。

数据流

react的单向数据流,vue的双向绑定,那么svelte是怎么样实现数据流的呢?

下面是我们业务中经常见到的代码,点击按钮请求数据然后设置到变量,触发dom内容的变化。svelte的写法形似vue的写法,但是它的runtime原理并没有双向绑定。编译后的代码除了有上面所说的create和mount等fragment生命周期属性外,其他代码更多表现了数据流的形式。

<script>
    let num = 1;

    async function handleClick() {
        const res = await fetch(`tutorial/random-number`);
        const text = await res.text();

        if (res.ok) {
            num = text;
            return text;
        } else {
            throw new Error(text);
        }
    }
</script>

<button on:click={handleClick}>
    generate random number
</button>


<p>The number is {num}</p>

编译出来的结果:

/* App.svelte generated by Svelte v3.24.0 */
import {
    SvelteComponent,
    append,
    detach,
    element,
    init,
    insert,
    listen,
    noop,
    safe_not_equal,
    set_data,
    space,
    text
} from "svelte/internal";

function create_fragment(ctx) {
    let button;
    let t1;
    let p;
    let t2;
    let t3;
    let mounted;
    let dispose;

    return {
        c() {
            button = element("button");
            button.textContent = "generate random number";
            t1 = space();
            p = element("p");
            t2 = text("The number is ");
            t3 = text(/*num*/ ctx[0]);
        },
        m(target, anchor) {
            insert(target, button, anchor);
            insert(target, t1, anchor);
            insert(target, p, anchor);
            append(p, t2);
            append(p, t3);

            if (!mounted) {
                dispose = listen(button, "click", /*handleClick*/ ctx[1]);
                mounted = true;
            }
        },
        p(ctx, [dirty]) {
            if (dirty & /*num*/ 1) set_data(t3, /*num*/ ctx[0]);
        },
        i: noop,
        o: noop,
        d(detaching) {
            if (detaching) detach(button);
            if (detaching) detach(t1);
            if (detaching) detach(p);
            mounted = false;
            dispose();
        }
    };
}

function instance($$self, $$props, $$invalidate) {
    let num = 1;

    async function handleClick() {
        const res = await fetch(`tutorial/random-number`);
        const text = await res.text();

        if (res.ok) {
            $$invalidate(0, num = text);
            return text;
        } else {
            throw new Error(text);
        }
    }

    return [num, handleClick];
}

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

export default App;

代码中,handleClick函数被封装在一个名为instance的方法当中,而它的入参当中有个$$invalidate的回调函数,用于变量的设置,把接口异步获取的数据设置回调函数当中。而它在组件的调用如下,重点在于回调函数当中,instance只会在初始化的时候调用,但是回调函数$$invalidate可以在各种异步情况调用。它会触发make_dirty的方法,而它触发了schedule_update,在一个微任务当中,触发flush将一段时间内的变量操作都执行掉。实现变量的处理,flush函数的具体实现请查看源码(src/runtime/internal/Component.ts)。flush的过程中会触发Fragment的update以及Component的update。

    $$.ctx = instance
        ? instance(component, prop_values, (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;
        })
        : [];

由此可见,svelte是单向数据流,很多数据工作已经在compile的过程当中已经完成。runtime更多是服务于浏览器层面的数据流转化。

题外话

shopee,又称虾皮,是一家腾讯投资的跨境电商平台。这里加班少,技术氛围好。如果想和我并肩作战一起学习,可以找我内推。邮箱weiping.xiang@shopee.com,非诚勿扰。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345